Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-24 10:50:03 +01:00
commit bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions

View file

@ -23,13 +23,12 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.tokens.utils.convertToAmount
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.error.SendTransactionError
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -45,7 +44,6 @@ import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.*
import com.tangem.lib.crypto.models.transactions.SendTxResult
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber
@ -62,24 +60,19 @@ internal class SwapInteractorImpl @Inject constructor(
private val allowPermissionsHandler: AllowPermissionsHandler,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
private val walletManagersFacade: WalletManagersFacade,
private val sendTransactionUseCase: SendTransactionUseCase,
private val createTransactionUseCase: CreateTransactionUseCase,
private val quotesRepository: QuotesRepository,
private val dispatcher: CoroutineDispatcherProvider,
private val swapTransactionRepository: SwapTransactionRepository,
private val currencyChecksRepository: CurrencyChecksRepository,
private val appCurrencyRepository: AppCurrencyRepository,
private val currenciesRepository: CurrenciesRepository,
private val initialToCurrencyResolver: InitialToCurrencyResolver,
private val demoConfig: DemoConfig,
private val transactionRepository: TransactionRepository,
private val validateTransactionUseCase: ValidateTransactionUseCase,
private val estimateFeeUseCase: EstimateFeeUseCase,
) : SwapInteractor {
private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) {
EstimateFeeUseCase(walletManagersFacade, dispatcher)
}
private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetSelectedAppCurrencyUseCase(appCurrencyRepository)
}
@ -494,35 +487,33 @@ internal class SwapInteractorImpl @Inject constructor(
),
)
transactionRepository.validateTransaction(
validateTransactionUseCase(
amount = amount.value.convertToAmount(fromToken),
fee = fee,
memo = null,
txExtras = null,
destination = getTokenAddress(fromToken),
userWalletId = userWalletId,
network = fromToken.network,
)
.fold(
onFailure = {
addCardanoTransactionValidationError(
warnings = warnings,
error = it as? BlockchainSdkError.Cardano ?: return@fold,
fromToken = fromToken,
userWalletId = userWalletId,
).fold(
ifLeft = {
addCardanoTransactionValidationError(
warnings = warnings,
error = it as? BlockchainSdkError.Cardano ?: return@fold,
fromToken = fromToken,
userWalletId = userWalletId,
)
},
ifRight = {
minAdaValue?.let {
warnings.add(
Warning.Cardano.MinAdaValueCharged(
tokenName = fromToken.name,
minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals),
),
)
},
onSuccess = {
minAdaValue?.let {
warnings.add(
Warning.Cardano.MinAdaValueCharged(
tokenName = fromToken.name,
minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals),
),
)
}
},
)
}
},
)
}
private suspend fun addCardanoTransactionValidationError(
@ -745,28 +736,27 @@ internal class SwapInteractorImpl @Inject constructor(
val exchangeDataCex =
exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError
val txExtras = transactionManager.getMemoExtras(
currencyToSend.currency.network.backendId,
exchangeDataCex.txExtraId,
)
val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError
if (txExtras == null && exchangeDataCex.txExtraId != null && !demoConfig.isDemoCardId(cardId)) {
return SwapTransactionState.UnknownError
}
if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.UnknownError
val txData = createTransactionUseCase(
amount = amount.value.convertToAmount(currencyToSend.currency),
fee = getFeeForTransaction(
fee = txFee,
blockchain = Blockchain.fromId(currencyToSend.currency.network.id.value),
),
memo = null,
memo = exchangeDataCex.txExtraId,
destination = exchangeDataCex.txTo,
userWalletId = userWalletId,
network = currencyToSend.currency.network,
).getOrElse {
Timber.e(it)
return SwapTransactionState.UnknownError
}.copy(extras = txExtras)
}
if (txData.extras == null && exchangeDataCex.txExtraId != null) {
return SwapTransactionState.UnknownError
}
val result = sendTransactionUseCase(
txData = txData,

View file

@ -12,7 +12,9 @@ import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
@ -46,11 +48,10 @@ class SwapDomainModule {
swapTransactionRepository: SwapTransactionRepository,
appCurrencyRepository: AppCurrencyRepository,
currencyChecksRepository: CurrencyChecksRepository,
walletManagersFacade: WalletManagersFacade,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
initialToCurrencyResolver: InitialToCurrencyResolver,
currenciesRepository: CurrenciesRepository,
transactionRepository: TransactionRepository,
validateTransactionUseCase: ValidateTransactionUseCase,
estimateFeeUseCase: EstimateFeeUseCase,
): SwapInteractor {
return SwapInteractorImpl(
transactionManager = transactionManager,
@ -62,15 +63,14 @@ class SwapDomainModule {
sendTransactionUseCase = sendTransactionUseCase,
createTransactionUseCase = createTransactionUseCase,
quotesRepository = quotesRepository,
walletManagersFacade = walletManagersFacade,
dispatcher = coroutineDispatcherProvider,
swapTransactionRepository = swapTransactionRepository,
appCurrencyRepository = appCurrencyRepository,
currencyChecksRepository = currencyChecksRepository,
currenciesRepository = currenciesRepository,
initialToCurrencyResolver = initialToCurrencyResolver,
demoConfig = DemoConfig(),
transactionRepository = transactionRepository,
validateTransactionUseCase = validateTransactionUseCase,
estimateFeeUseCase = estimateFeeUseCase,
)
}

View file

@ -19,7 +19,7 @@ dependencies {
implementation(projects.core.navigation)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.common)
implementation(projects.common.routing)
/** Domain modules **/
implementation(projects.domain.appCurrency)

View file

@ -2,12 +2,13 @@ package com.tangem.feature.swap.presentation
import android.os.Bundle
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.swap.router.CustomTabsManager
@ -28,7 +29,7 @@ class SwapFragment : ComposeFragment() {
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var reduxNavController: ReduxNavController
lateinit var appRouter: AppRouter
private val viewModel by viewModels<SwapViewModel>()
@ -37,9 +38,8 @@ class SwapFragment : ComposeFragment() {
lifecycle.addObserver(viewModel)
viewModel.setRouter(
SwapRouter(
fragmentManager = WeakReference(parentFragmentManager),
customTabsManager = CustomTabsManager(WeakReference(context)),
reduxNavController = reduxNavController,
router = appRouter,
),
)
}
@ -47,17 +47,17 @@ class SwapFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
viewModel.onScreenOpened()
val backgroundColor = TangemTheme.colors.background.secondary
SystemBarsEffect { setSystemBarsColor(backgroundColor) }
ScreenContent(viewModel = viewModel)
}
@Suppress("TopLevelComposableFunctions")
@Composable
private fun ScreenContent(viewModel: SwapViewModel) {
Crossfade(targetState = viewModel.currentScreen, label = "") { screen ->
Crossfade(
modifier = Modifier.background(TangemTheme.colors.background.secondary),
targetState = viewModel.currentScreen,
label = "",
) { screen ->
when (screen) {
SwapNavScreen.Main -> SwapScreen(stateHolder = viewModel.uiState)
SwapNavScreen.Success -> {
@ -84,8 +84,4 @@ class SwapFragment : ComposeFragment() {
lifecycle.removeObserver(viewModel)
super.onDestroy()
}
companion object {
const val CURRENCY_BUNDLE_KEY = "swap_currency"
}
}

View file

@ -3,20 +3,14 @@ package com.tangem.feature.swap.router
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
import java.lang.ref.WeakReference
internal class SwapRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val customTabsManager: CustomTabsManager,
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
) {
var currentScreen by mutableStateOf(SwapNavScreen.Main)
@ -30,7 +24,7 @@ internal class SwapRouter(
if (currentScreen == SwapNavScreen.SelectToken) {
currentScreen = SwapNavScreen.Main
} else {
fragmentManager.get()?.popBackStack()
router.pop()
}
}
@ -39,13 +33,10 @@ internal class SwapRouter(
}
fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.WalletDetails,
bundle = bundleOf(
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency,
),
router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = currency,
),
)
}

View file

@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
@ -28,7 +29,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) {
iconRes = R.drawable.ic_close_24,
)
},
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
contentWindowInsets = WindowInsetsZero,
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.common.Strings.STARS
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
@ -38,6 +37,7 @@ import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
import com.tangem.utils.Strings.STARS
@Suppress("LongMethod")
@Composable

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.Strings
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.ExpandableSearchView
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
@ -30,6 +29,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.R
import com.tangem.utils.Strings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList

View file

@ -1,11 +1,14 @@
package com.tangem.feature.swap.viewmodels
import android.os.Bundle
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
@ -32,7 +35,6 @@ import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
import com.tangem.feature.swap.ui.StateBuilder
@ -70,8 +72,10 @@ internal class SwapViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver {
private val initialCryptoCurrency: CryptoCurrency =
savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`")
private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get<Bundle>(AppRoute.Swap.CURRENCY_BUNDLE_KEY)
?.let { it.unbundle(CryptoCurrency.serializer()) }
?: error("no expected parameter CryptoCurrency found`")
private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus
private var isBalanceHidden = true