Updated on 2026-08-14
This commit is contained in:
commit
1056edbfa2
84 changed files with 891 additions and 215 deletions
|
|
@ -87,6 +87,7 @@ dependencies {
|
|||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.swap.presentation)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.data)
|
||||
implementation(projects.features.tester.api)
|
||||
implementation(projects.features.tester.impl)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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.SettingsRepository
|
||||
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
|
||||
import dagger.Module
|
||||
|
|
@ -103,4 +104,20 @@ internal object SettingsDomainModule {
|
|||
fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled {
|
||||
return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideShouldShowSwapPromoWalletUseCase(
|
||||
swapPromoRepository: SwapPromoRepository,
|
||||
): ShouldShowSwapPromoWalletUseCase {
|
||||
return ShouldShowSwapPromoWalletUseCase(swapPromoRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideShouldShowSwapPromoTokenUseCase(
|
||||
swapPromoRepository: SwapPromoRepository,
|
||||
): ShouldShowSwapPromoTokenUseCase {
|
||||
return ShouldShowSwapPromoTokenUseCase(swapPromoRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -83,6 +85,9 @@ internal object TokensDomainModule {
|
|||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
networksRepository: NetworksRepository,
|
||||
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
|
||||
swapRepository: SwapRepository,
|
||||
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetCurrencyWarningsUseCase {
|
||||
return GetCurrencyWarningsUseCase(
|
||||
|
|
@ -90,6 +95,9 @@ internal object TokensDomainModule {
|
|||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
|
||||
swapRepository = swapRepository,
|
||||
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun estimateFee(amount: Amount): Result<TransactionFee> {
|
||||
return getFee(amount, walletManager.wallet.address)
|
||||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val signerResponse = signer.sign(
|
||||
hash = getDataToSign(),
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@ object PreferencesKeys {
|
|||
val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") }
|
||||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
|
||||
val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") }
|
||||
|
||||
val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") }
|
||||
}
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
|
|
|
|||
|
|
@ -244,6 +244,8 @@
|
|||
<string name="main_promotion_credited">1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов</string>
|
||||
<string name="main_scan_card_warning_view_subtitle">Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту</string>
|
||||
<string name="main_scan_card_warning_view_title">Отсканируйте карту</string>
|
||||
<string name="main_swap_promotion_message">Обменивайте свои цифровые активы между различными сетями</string>
|
||||
<string name="main_swap_promotion_title">Кроссчейн-своп теперь доступен</string>
|
||||
<string name="main_tokens">Токены</string>
|
||||
<string name="manage_tokens_add">Добавить</string>
|
||||
<string name="manage_tokens_edit">Изменить</string>
|
||||
|
|
@ -539,6 +541,9 @@
|
|||
<string name="token_details_unable_hide_alert_message">Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>
|
||||
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
|
||||
<string name="token_item_no_rate">Нет цены</string>
|
||||
<string name="token_swap_promotion_message">Обменяйте этот токен на другие активы в вашем портфеле</string>
|
||||
<string name="token_swap_promotion_title">Представляем кроссчейн-своп</string>
|
||||
<string name="token_swap_promotion_button">Обменять</string>
|
||||
<string name="transaction_history_contract_address">контракт: %s</string>
|
||||
<string name="transaction_history_empty_transactions">У вас еще нет транзакций</string>
|
||||
<string name="transaction_history_error_failed_to_load">Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию.</string>
|
||||
|
|
|
|||
|
|
@ -263,6 +263,8 @@
|
|||
<string name="main_promotion_credited">1INCH tokens will be credited to your %s wallet address within 48 hours</string>
|
||||
<string name="main_scan_card_warning_view_subtitle">To access all the networks you need to scan the card</string>
|
||||
<string name="main_scan_card_warning_view_title">Scan your card</string>
|
||||
<string name="main_swap_promotion_message">Swap multiple currencies across several blockchains</string>
|
||||
<string name="main_swap_promotion_title">Cross-chain swaps are now available</string>
|
||||
<string name="main_tokens">Tokens</string>
|
||||
<string name="manage_tokens_add">Add</string>
|
||||
<string name="manage_tokens_edit">Edit</string>
|
||||
|
|
@ -569,6 +571,9 @@
|
|||
<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_title">Unable to hide %s</string>
|
||||
<string name="token_item_no_rate">No rate</string>
|
||||
<string name="token_swap_promotion_message">Exchange this token for other assets in your portfolio</string>
|
||||
<string name="token_swap_promotion_title">Introducing cross-chain swaps</string>
|
||||
<string name="token_swap_promotion_button">Exchange now</string>
|
||||
<string name="transaction_history_contract_address">contract: %s</string>
|
||||
<string name="transaction_history_empty_transactions">You don\'t have any transactions yet</string>
|
||||
<string name="transaction_history_error_failed_to_load">Failed to load transaction history.\nClick on reload button to update the information.</string>
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
/**
|
||||
* Notification component state
|
||||
*
|
||||
* @property title title
|
||||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property buttonsState buttons state
|
||||
* @property onClick lambda be invoked when notification is clicked
|
||||
* @property onCloseClick lambda be invoked when close button is clicked
|
||||
* @property title title
|
||||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property backgroundResId background resource id
|
||||
* @property buttonsState buttons state
|
||||
* @property onClick lambda be invoked when notification is clicked
|
||||
* @property onCloseClick lambda be invoked when close button is clicked
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -19,6 +20,7 @@ data class NotificationConfig(
|
|||
val title: TextReference,
|
||||
val subtitle: TextReference,
|
||||
@DrawableRes val iconResId: Int,
|
||||
@DrawableRes val backgroundResId: Int? = null,
|
||||
val buttonsState: ButtonsState? = null,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
val onCloseClick: (() -> Unit)? = null,
|
||||
|
|
@ -33,7 +35,11 @@ data class NotificationConfig(
|
|||
val onClick: () -> Unit,
|
||||
) : ButtonsState()
|
||||
|
||||
data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState()
|
||||
data class SecondaryButtonConfig(
|
||||
val text: TextReference,
|
||||
@DrawableRes val iconResId: Int? = null,
|
||||
val onClick: () -> Unit,
|
||||
) : ButtonsState()
|
||||
|
||||
data class PairButtonsConfig(
|
||||
val primaryText: TextReference,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.constraintlayout.compose.Dimension
|
||||
import androidx.constraintlayout.compose.Visibility
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SecondaryButtonIconStart
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Custom notification with image background
|
||||
* @see [Swap Promo](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=8713-6307&mode=dev)
|
||||
*/
|
||||
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
|
||||
@Composable
|
||||
fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) {
|
||||
val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = TangemTheme.dimens.size62)
|
||||
.fillMaxWidth()
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium),
|
||||
) {
|
||||
val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs()
|
||||
|
||||
val spacing2 = TangemTheme.dimens.spacing2
|
||||
val spacing12 = TangemTheme.dimens.spacing12
|
||||
val spacing14 = TangemTheme.dimens.spacing14
|
||||
|
||||
Image(
|
||||
painter = painterResource(config.backgroundResId ?: R.drawable.img_swap_promo_banner_background),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.constrainAs(backgroundRef) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(parent.start)
|
||||
end.linkTo(parent.end)
|
||||
bottom.linkTo(parent.bottom)
|
||||
width = Dimension.fillToConstraints
|
||||
height = Dimension.fillToConstraints
|
||||
visibility = if (config.backgroundResId == null) Visibility.Gone else Visibility.Visible
|
||||
},
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(config.iconResId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.constrainAs(iconRef) {
|
||||
start.linkTo(parent.start, spacing12)
|
||||
linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f)
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = config.title.resolveReference(),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
modifier = Modifier.constrainAs(titleRef) {
|
||||
top.linkTo(parent.top, spacing12)
|
||||
start.linkTo(iconRef.end, spacing12)
|
||||
end.linkTo(closeIconRef.start, spacing2)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
Text(
|
||||
text = config.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
modifier = Modifier.constrainAs(subtitleRef) {
|
||||
top.linkTo(titleRef.bottom, spacing2)
|
||||
start.linkTo(iconRef.end, spacing12)
|
||||
end.linkTo(parent.end, spacing12)
|
||||
bottom.linkTo(buttonRef.top, spacing12, spacing14)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16)
|
||||
.constrainAs(closeIconRef) {
|
||||
top.linkTo(parent.top, spacing12)
|
||||
end.linkTo(parent.end, spacing12)
|
||||
}
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) {
|
||||
config.onCloseClick?.invoke()
|
||||
},
|
||||
)
|
||||
SecondaryButtonIconStart(
|
||||
text = button?.text?.resolveReference().orEmpty(),
|
||||
iconResId = button?.iconResId ?: R.drawable.ic_exchange_vertical_24,
|
||||
onClick = button?.onClick ?: {},
|
||||
modifier = Modifier.constrainAs(buttonRef) {
|
||||
start.linkTo(parent.start, spacing12)
|
||||
end.linkTo(parent.end, spacing12)
|
||||
bottom.linkTo(parent.bottom, spacing12)
|
||||
width = Dimension.fillToConstraints
|
||||
visibility = if (button == null) {
|
||||
Visibility.Gone
|
||||
} else {
|
||||
Visibility.Visible
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun NotificationWithBackgroundPreview(
|
||||
@PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig,
|
||||
) {
|
||||
TangemTheme {
|
||||
NotificationWithBackground(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider<NotificationConfig> {
|
||||
override val values: Sequence<NotificationConfig>
|
||||
get() = sequenceOf(
|
||||
NotificationConfig(
|
||||
title = resourceReference(id = R.string.main_swap_promotion_title),
|
||||
subtitle = resourceReference(id = R.string.main_swap_promotion_message),
|
||||
iconResId = R.drawable.ic_swap_promo_34,
|
||||
backgroundResId = R.drawable.img_swap_promo_banner_background,
|
||||
),
|
||||
NotificationConfig(
|
||||
title = resourceReference(id = R.string.token_swap_promotion_title),
|
||||
subtitle = stringReference(
|
||||
"Swap multiple currencies between any chains you wish. Swap multiple " +
|
||||
"currencies between any chains you wish. Swap multiple " +
|
||||
"currencies between any chains you wish. Commission free period " +
|
||||
"till Dec 31.",
|
||||
),
|
||||
iconResId = R.drawable.ic_swap_promo_34,
|
||||
backgroundResId = R.drawable.img_swap_promo_banner_background,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(id = R.string.token_swap_promotion_button),
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -71,7 +71,6 @@ fun LazyListScope.txHistoryItems(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun LazyListScope.contentItems(
|
||||
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>,
|
||||
isBalanceHidden: Boolean,
|
||||
|
|
@ -93,7 +92,6 @@ private fun LazyListScope.contentItems(
|
|||
state = item,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = modifier
|
||||
.animateItemPlacement()
|
||||
.roundedShapeItemDecoration(
|
||||
currentIndex = index,
|
||||
lastIndex = txHistoryItems.itemSnapshotList.lastIndex,
|
||||
|
|
|
|||
20
core/ui/src/main/res/drawable/ic_swap_promo_34.xml
Normal file
20
core/ui/src/main/res/drawable/ic_swap_promo_34.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="34dp"
|
||||
android:height="34dp"
|
||||
android:viewportWidth="34"
|
||||
android:viewportHeight="34">
|
||||
<path
|
||||
android:pathData="M22,0C28.628,0 34,5.373 34,12C34,18.628 28.628,24 22,24C15.373,24 10,18.628 10,12C10,5.373 15.373,0 22,0Z"
|
||||
android:fillColor="#F5AC37"/>
|
||||
<path
|
||||
android:pathData="M22.442,12.847H27.002C27.099,12.847 27.145,12.847 27.152,12.72C27.189,12.256 27.189,11.79 27.152,11.325C27.152,11.235 27.107,11.198 27.01,11.198H17.935C17.823,11.198 17.792,11.235 17.792,11.34V12.675C17.792,12.847 17.792,12.847 17.972,12.847H22.442ZM26.643,9.638C26.656,9.604 26.656,9.566 26.643,9.533C26.567,9.367 26.477,9.209 26.372,9.06C26.214,8.807 26.028,8.573 25.817,8.363C25.717,8.236 25.601,8.122 25.472,8.025C24.823,7.473 24.051,7.082 23.221,6.885C22.803,6.791 22.375,6.746 21.947,6.75H17.919C17.807,6.75 17.792,6.795 17.792,6.893V9.555C17.792,9.667 17.792,9.698 17.934,9.698H26.589C26.589,9.698 26.664,9.682 26.679,9.638H26.642H26.643ZM26.643,14.407C26.515,14.393 26.387,14.393 26.26,14.407H17.942C17.83,14.407 17.792,14.407 17.792,14.558V17.16C17.792,17.28 17.792,17.31 17.942,17.31H21.782C21.966,17.325 22.149,17.312 22.329,17.273C22.887,17.233 23.435,17.112 23.958,16.913C24.147,16.847 24.331,16.761 24.504,16.658H24.557C25.457,16.19 26.188,15.455 26.649,14.552C26.649,14.552 26.702,14.438 26.643,14.408V14.407ZM16.285,18.66V18.615V16.868V16.275V14.513C16.285,14.415 16.285,14.4 16.165,14.4H14.538C14.447,14.4 14.41,14.4 14.41,14.281V12.856H16.15C16.247,12.856 16.285,12.856 16.285,12.728V11.318C16.285,11.228 16.285,11.206 16.165,11.206H14.538C14.447,11.206 14.41,11.206 14.41,11.086V9.766C14.41,9.683 14.41,9.661 14.53,9.661H16.142C16.255,9.661 16.285,9.661 16.285,9.519V5.476C16.285,5.356 16.285,5.326 16.435,5.326H22.06C22.468,5.342 22.874,5.387 23.275,5.461C24.102,5.614 24.896,5.909 25.622,6.331C26.104,6.615 26.547,6.957 26.942,7.351C27.239,7.66 27.507,7.993 27.744,8.349C27.98,8.709 28.177,9.094 28.33,9.496C28.349,9.601 28.45,9.672 28.555,9.654H29.897C30.069,9.654 30.069,9.654 30.077,9.819V11.049C30.077,11.169 30.032,11.199 29.912,11.199H28.876C28.772,11.199 28.741,11.199 28.749,11.334C28.79,11.791 28.79,12.25 28.749,12.706C28.749,12.834 28.749,12.849 28.892,12.849H30.076C30.129,12.917 30.076,12.984 30.076,13.052C30.084,13.139 30.084,13.227 30.076,13.314V14.222C30.076,14.349 30.039,14.387 29.926,14.387H28.509C28.41,14.368 28.314,14.431 28.291,14.53C27.954,15.407 27.413,16.194 26.716,16.824C26.461,17.054 26.193,17.27 25.913,17.469C25.613,17.642 25.321,17.822 25.013,17.965C24.447,18.219 23.853,18.407 23.243,18.527C22.664,18.63 22.077,18.677 21.488,18.669H16.283V18.662L16.285,18.66Z"
|
||||
android:fillColor="#FEFEFD"/>
|
||||
<path
|
||||
android:pathData="M8.063,10.661C3.37,12.29 0,16.751 0,22C0,28.627 5.373,34 12,34C17.249,34 21.71,30.631 23.339,25.937C22.898,25.979 22.452,26 22,26C14.269,26 8,19.732 8,12C8,11.548 8.021,11.101 8.063,10.661Z"
|
||||
android:fillColor="#8247E5"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M8.324,15.007C8.19,15.022 8.055,15.061 7.927,15.125L4.501,17.131C4.167,17.298 4,17.632 4,17.966V21.893C4,22.227 4.167,22.562 4.501,22.77L7.885,24.734C8.178,24.901 8.554,24.901 8.888,24.734L11.186,23.397L12.738,22.498C12.328,22.136 11.939,21.751 11.574,21.344L11.186,21.559L8.888,22.937C8.595,23.105 8.219,23.105 7.885,22.937L6.089,21.851C5.796,21.684 5.587,21.35 5.587,20.974V18.885C5.587,18.551 5.754,18.217 6.089,18.008L7.885,16.964C8.178,16.796 8.554,16.796 8.888,16.964L8.911,16.978C8.668,16.341 8.472,15.683 8.324,15.007ZM20,25.858C19.423,25.776 18.857,25.658 18.305,25.507C18.228,25.711 18.083,25.895 17.87,26.029L16.115,27.073C15.822,27.24 15.446,27.24 15.112,27.073L13.316,26.029C13.023,25.862 12.814,25.528 12.814,25.152V23.815L11.227,24.734V26.112C11.227,26.447 11.394,26.781 11.729,26.99L15.112,28.953C15.211,29.01 15.319,29.047 15.432,29.065C15.654,29.101 15.894,29.064 16.115,28.953L19.499,26.99C19.791,26.823 20,26.488 20,26.112V25.858Z"
|
||||
android:fillColor="#FEFEFD"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,55 @@
|
|||
package com.tangem.data.settings
|
||||
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_SHOW_KEY
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_SHOW_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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* Repository for showing swap promo notification.
|
||||
*/
|
||||
class DefaultSwapPromoRepository(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : SwapPromoRepository {
|
||||
override fun isReadyToShowWallet(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_SHOW_KEY, true)
|
||||
.map { it && checkPromoPeriod() }
|
||||
}
|
||||
|
||||
override fun isReadyToShowToken(): Flow<Boolean> {
|
||||
return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, true)
|
||||
.map { it && checkPromoPeriod() }
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowWallet() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_WALLET_SWAP_PROMO_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun setNeverToShowToken() {
|
||||
appPreferencesStore.store(
|
||||
key = IS_TOKEN_SWAP_PROMO_SHOW_KEY,
|
||||
value = false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun checkPromoPeriod(): Boolean {
|
||||
val currentDate = Calendar.getInstance()
|
||||
return currentDate.get(Calendar.DATE) in START_DATE_KEY..END_DATE_KEY &&
|
||||
currentDate.get(Calendar.MONTH) == MONTH_KEY && currentDate.get(Calendar.YEAR) == YEAR_KEY
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val START_DATE_KEY = 15
|
||||
private const val END_DATE_KEY = 31
|
||||
private const val MONTH_KEY = 11 // December
|
||||
private const val YEAR_KEY = 2023 // just in case
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,12 @@ 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.source.preferences.PreferencesDataSource
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -36,4 +38,10 @@ internal object SettingsDataModule {
|
|||
fun provideAppRatingRepository(appPreferencesStore: AppPreferencesStore): AppRatingRepository {
|
||||
return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository {
|
||||
return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ import timber.log.Timber
|
|||
import java.math.BigDecimal
|
||||
import java.util.EnumSet
|
||||
|
||||
@Suppress("LargeClass")
|
||||
@Suppress("LargeClass", "TooManyFunctions")
|
||||
// FIXME: Move to its own module and make internal
|
||||
@Deprecated("Inject the WalletManagerFacade interface using DI instead")
|
||||
class DefaultWalletManagersFacade(
|
||||
|
|
@ -433,6 +433,20 @@ class DefaultWalletManagersFacade(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun estimateFee(
|
||||
amount: Amount,
|
||||
userWalletId: UserWalletId,
|
||||
network: Network,
|
||||
): Result<TransactionFee>? {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
val walletManager = getOrCreateWalletManager(
|
||||
userWalletId = userWalletId,
|
||||
blockchain = blockchain,
|
||||
derivationPath = network.derivationPath.value,
|
||||
)
|
||||
return (walletManager as? TransactionSender)?.estimateFee(amount = amount)
|
||||
}
|
||||
|
||||
override suspend fun validateTransaction(
|
||||
amount: Amount,
|
||||
fee: Amount?,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import java.util.EnumSet
|
|||
/**
|
||||
* A facade for managing wallets.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface WalletManagersFacade {
|
||||
|
||||
/**
|
||||
|
|
@ -166,6 +167,15 @@ interface WalletManagersFacade {
|
|||
network: Network,
|
||||
): Result<TransactionFee>?
|
||||
|
||||
/**
|
||||
* Returns estimated fee for transaction
|
||||
*
|
||||
* @param amount of transaction
|
||||
* @param userWalletId selected wallet id
|
||||
* @param network network of currency
|
||||
*/
|
||||
suspend fun estimateFee(amount: Amount, userWalletId: UserWalletId, network: Network): Result<TransactionFee>?
|
||||
|
||||
/**
|
||||
* Validates transaction
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.settings
|
||||
|
||||
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) {
|
||||
|
||||
operator fun invoke(): Flow<Boolean> = swapPromoRepository.isReadyToShowToken()
|
||||
|
||||
suspend fun neverToShow() = swapPromoRepository.setNeverToShowToken()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.domain.settings
|
||||
|
||||
import com.tangem.domain.settings.repositories.SwapPromoRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) {
|
||||
|
||||
operator fun invoke(): Flow<Boolean> = swapPromoRepository.isReadyToShowWallet()
|
||||
|
||||
suspend fun neverToShow() = swapPromoRepository.setNeverToShowWallet()
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.domain.settings.repositories
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SwapPromoRepository {
|
||||
fun isReadyToShowWallet(): Flow<Boolean>
|
||||
|
||||
fun isReadyToShowToken(): Flow<Boolean>
|
||||
|
||||
suspend fun setNeverToShowWallet()
|
||||
|
||||
suspend fun setNeverToShowToken()
|
||||
}
|
||||
|
|
@ -18,6 +18,9 @@ dependencies {
|
|||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
|
||||
/** Project - Other */
|
||||
implementation(projects.core.utils)
|
||||
|
|
|
|||
|
|
@ -31,4 +31,6 @@ sealed class CryptoCurrencyWarning {
|
|||
data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning()
|
||||
|
||||
data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning()
|
||||
|
||||
object SwapPromo : CryptoCurrencyWarning()
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.domain.tokens.models.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
sealed class TokenSwapPromoAnalyticsEvent(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Swap Promo", event, params, null) {
|
||||
|
||||
object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close")
|
||||
|
||||
class Exchange(
|
||||
token: String,
|
||||
) : TokenSwapPromoAnalyticsEvent(
|
||||
event = "Button - Exchange Now",
|
||||
params = mapOf("Token" to token),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,24 +1,33 @@
|
|||
package com.tangem.domain.tokens
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
class GetCurrencyWarningsUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val currenciesRepository: CurrenciesRepository,
|
||||
private val quotesRepository: QuotesRepository,
|
||||
private val networksRepository: NetworksRepository,
|
||||
private val swapRepository: SwapRepository,
|
||||
private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
|
||||
private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
|
|
@ -29,20 +38,32 @@ class GetCurrencyWarningsUseCase(
|
|||
isSingleWalletWithTokens: Boolean,
|
||||
): Flow<Set<CryptoCurrencyWarning>> {
|
||||
val currency = currencyStatus.currency
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
userWalletId = userWalletId,
|
||||
)
|
||||
return combine(
|
||||
getCoinRelatedWarnings(
|
||||
userWalletId = userWalletId,
|
||||
operations = operations,
|
||||
networkId = currency.network.id,
|
||||
currencyId = currency.id,
|
||||
derivationPath = derivationPath,
|
||||
isSingleWalletWithTokens = isSingleWalletWithTokens,
|
||||
),
|
||||
operations.getCurrenciesStatusesFlow().conflate(),
|
||||
flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)),
|
||||
flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)),
|
||||
flowOf(getNetworkUnavailableWarning(currencyStatus)),
|
||||
flowOf(getNetworkNoAccountWarning(currencyStatus)),
|
||||
) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable, maybeNetworkNoAccount ->
|
||||
showSwapPromoTokenUseCase().conflate(),
|
||||
) { coinRelatedWarnings, cryptoStatuses, maybeRentWarning, maybeEdWarning, shouldShowSwapPromo ->
|
||||
setOfNotNull(
|
||||
getSwapPromoNotificationWarning(
|
||||
shouldShowSwapPromo = shouldShowSwapPromo,
|
||||
userWalletId = userWalletId,
|
||||
currencyStatus = currencyStatus,
|
||||
cryptoStatuses = cryptoStatuses,
|
||||
),
|
||||
maybeRentWarning,
|
||||
maybeEdWarning?.let {
|
||||
CryptoCurrencyWarning.ExistentialDeposit(
|
||||
|
|
@ -51,27 +72,69 @@ class GetCurrencyWarningsUseCase(
|
|||
)
|
||||
},
|
||||
*coinRelatedWarnings.toTypedArray(),
|
||||
maybeNetworkUnavailable,
|
||||
maybeNetworkNoAccount,
|
||||
getNetworkUnavailableWarning(currencyStatus),
|
||||
getNetworkNoAccountWarning(currencyStatus),
|
||||
)
|
||||
}.flowOn(dispatchers.io)
|
||||
}
|
||||
|
||||
private suspend fun getSwapPromoNotificationWarning(
|
||||
shouldShowSwapPromo: Boolean,
|
||||
userWalletId: UserWalletId,
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
cryptoStatuses: Either<CurrenciesStatusesOperations.Error, List<CryptoCurrencyStatus>>,
|
||||
): CryptoCurrencyWarning? {
|
||||
val currency = currencyStatus.currency
|
||||
return if (shouldShowSwapPromo && marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)) {
|
||||
cryptoStatuses.fold(
|
||||
ifLeft = { null },
|
||||
ifRight = { cryptoCurrencyStatuses ->
|
||||
val pairs = swapRepository.getPairs(
|
||||
LeastTokenInfo(
|
||||
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0",
|
||||
network = currency.network.backendId,
|
||||
),
|
||||
cryptoCurrencyStatuses.map { it.currency },
|
||||
)
|
||||
val filteredCurrencies = cryptoCurrencyStatuses.filterNot {
|
||||
it.currency.network.backendId == currency.network.backendId
|
||||
}
|
||||
val currencyPairs = pairs.pairs.filter {
|
||||
it.from.network == currency.network.backendId ||
|
||||
it.to.network == currency.network.backendId
|
||||
}
|
||||
val isExchangeable = currencyPairs.any { pair ->
|
||||
val availablePair = if (currencyStatus.value.amount.isZero()) {
|
||||
filteredCurrencies.filterNot { it.value.amount.isZero() }
|
||||
} else {
|
||||
filteredCurrencies
|
||||
}
|
||||
availablePair
|
||||
.any {
|
||||
it.currency.network.backendId == pair.to.network ||
|
||||
it.currency.network.backendId == pair.from.network
|
||||
}
|
||||
}
|
||||
if (isExchangeable) {
|
||||
CryptoCurrencyWarning.SwapPromo
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private suspend fun getCoinRelatedWarnings(
|
||||
userWalletId: UserWalletId,
|
||||
operations: CurrenciesStatusesOperations,
|
||||
networkId: Network.ID,
|
||||
currencyId: CryptoCurrency.ID,
|
||||
derivationPath: Network.DerivationPath,
|
||||
isSingleWalletWithTokens: Boolean,
|
||||
): Flow<List<CryptoCurrencyWarning>> {
|
||||
val operations = CurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
networksRepository = networksRepository,
|
||||
userWalletId = userWalletId,
|
||||
)
|
||||
|
||||
val currencyFlow = if (isSingleWalletWithTokens) {
|
||||
operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
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.AmountType
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Use case to estimate transaction fee
|
||||
*/
|
||||
class EstimateFeeUseCase(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val dispatcher: CoroutineDispatcherProvider,
|
||||
) {
|
||||
suspend operator fun invoke(
|
||||
amount: BigDecimal,
|
||||
userWalletId: UserWalletId,
|
||||
cryptoCurrency: CryptoCurrency,
|
||||
): Flow<Either<GetFeeError.DataError, TransactionFee>> {
|
||||
return flow {
|
||||
val result = walletManagersFacade.estimateFee(
|
||||
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrency.network,
|
||||
)
|
||||
|
||||
val maybeFee = when (result) {
|
||||
is Result.Success -> result.data.right()
|
||||
else -> GetFeeError.DataError.left()
|
||||
}
|
||||
emit(maybeFee)
|
||||
}.flowOn(dispatcher.io)
|
||||
}
|
||||
|
||||
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
|
||||
currencySymbol = cryptoCurrency.symbol,
|
||||
value = amount,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
type = when (cryptoCurrency) {
|
||||
is CryptoCurrency.Coin -> AmountType.Coin
|
||||
is CryptoCurrency.Token -> AmountType.Token(
|
||||
token = Token(
|
||||
symbol = cryptoCurrency.symbol,
|
||||
contractAddress = cryptoCurrency.contractAddress,
|
||||
decimals = cryptoCurrency.decimals,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -15,6 +15,8 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
|
||||
/** Network */
|
||||
implementation(deps.retrofit)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.feature.swap
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.left
|
||||
import arrow.core.raise.catch
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.right
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.Approver
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -27,12 +29,11 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.converters.*
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.createFromAmountWithOffset
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -184,7 +185,7 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
): AggregatedSwapDataModel<QuoteModel> {
|
||||
): Either<DataError, QuoteModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
val response = tangemExpressApi.getExchangeQuote(
|
||||
|
|
@ -198,14 +199,12 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
providerId = providerId,
|
||||
rateType = rateType.name.lowercase(),
|
||||
).getOrThrow()
|
||||
AggregatedSwapDataModel(
|
||||
dataModel = QuoteModel(
|
||||
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
|
||||
allowanceContract = response.allowanceContract,
|
||||
),
|
||||
)
|
||||
QuoteModel(
|
||||
toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals),
|
||||
allowanceContract = response.allowanceContract,
|
||||
).right()
|
||||
} catch (ex: Exception) {
|
||||
AggregatedSwapDataModel(null, getDataError(ex))
|
||||
getDataError(ex).left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -227,7 +226,7 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
providerId: String,
|
||||
rateType: RateType,
|
||||
toAddress: String,
|
||||
): AggregatedSwapDataModel<SwapDataModel> {
|
||||
): Either<DataError, SwapDataModel> {
|
||||
return withContext(coroutineDispatcher.io) {
|
||||
try {
|
||||
val response = tangemExpressApi.getExchangeData(
|
||||
|
|
@ -242,11 +241,9 @@ internal class SwapRepositoryImpl @Inject constructor(
|
|||
rateType = rateType.name.lowercase(),
|
||||
toAddress = toAddress,
|
||||
).getOrThrow()
|
||||
AggregatedSwapDataModel(
|
||||
dataModel = expressDataConverter.convert(response),
|
||||
)
|
||||
expressDataConverter.convert(response).right()
|
||||
} catch (ex: Exception) {
|
||||
AggregatedSwapDataModel(null, getDataError(ex))
|
||||
getDataError(ex).left()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
|||
import com.tangem.feature.swap.SwapRepositoryImpl
|
||||
import com.tangem.feature.swap.converters.ErrorsDataConverter
|
||||
import com.tangem.feature.swap.DefaultSwapTransactionRepository
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
|
|||
1
features/swap/domain/api/.gitignore
vendored
Normal file
1
features/swap/domain/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
18
features/swap/domain/api/build.gradle.kts
Normal file
18
features/swap/domain/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.feature.swap.domain.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.swap.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
package com.tangem.feature.swap.domain.api
|
||||
|
||||
import arrow.core.Either
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface SwapRepository {
|
||||
|
|
@ -28,7 +28,7 @@ interface SwapRepository {
|
|||
toDecimals: Int,
|
||||
providerId: String,
|
||||
rateType: RateType,
|
||||
): AggregatedSwapDataModel<QuoteModel>
|
||||
): Either<DataError, QuoteModel>
|
||||
|
||||
/**
|
||||
* Returns address of 1inch router that must be trusted
|
||||
|
|
@ -77,7 +77,7 @@ interface SwapRepository {
|
|||
providerId: String,
|
||||
rateType: RateType,
|
||||
toAddress: String,
|
||||
): AggregatedSwapDataModel<SwapDataModel>
|
||||
): Either<DataError, SwapDataModel>
|
||||
|
||||
fun getNativeTokenForNetwork(networkId: String): CryptoCurrency
|
||||
}
|
||||
|
|
@ -29,6 +29,8 @@ dependencies {
|
|||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.appCurrency.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.utils)
|
||||
|
|
@ -38,7 +40,6 @@ dependencies {
|
|||
implementation(projects.features.wallet.api)
|
||||
|
||||
/** Other Libraries **/
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.timber)
|
||||
|
|
|
|||
1
features/swap/domain/models/.gitignore
vendored
Normal file
1
features/swap/domain/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
23
features/swap/domain/models/build.gradle.kts
Normal file
23
features/swap/domain/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.feature.swap.domain.models"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Domain */
|
||||
implementation(projects.domain.tokens.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Other Libraries **/
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.arrow.core)
|
||||
|
||||
}
|
||||
|
|
@ -5,7 +5,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
|
||||
interface SwapInteractor {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.feature.swap.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.AmountType
|
||||
|
|
@ -13,15 +14,16 @@ import com.tangem.domain.tokens.model.Quote
|
|||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.utils.convertToAmount
|
||||
import com.tangem.domain.transaction.error.SendTransactionError
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.EstimateFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
|
|
@ -54,8 +56,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
private val initialToCurrencyResolver: InitialToCurrencyResolver,
|
||||
) : SwapInteractor {
|
||||
|
||||
private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
GetFeeUseCase(walletManagersFacade, dispatcher)
|
||||
private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
EstimateFeeUseCase(walletManagersFacade, dispatcher)
|
||||
}
|
||||
|
||||
private val swapCurrencyConverter = SwapCurrencyConverter()
|
||||
|
|
@ -297,13 +299,14 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
)
|
||||
|
||||
val fromTokenAddress = getTokenAddress(fromToken.currency)
|
||||
val isAllowedToSpend = if (quotes.dataModel != null) {
|
||||
quotes.dataModel.allowanceContract?.let {
|
||||
isAllowedToSpend(networkId, fromToken.currency, amount, it)
|
||||
} ?: true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val isAllowedToSpend = quotes.fold(
|
||||
ifRight = {
|
||||
it.allowanceContract?.let {
|
||||
isAllowedToSpend(networkId, fromToken.currency, amount, it)
|
||||
} ?: true
|
||||
},
|
||||
ifLeft = { false },
|
||||
)
|
||||
|
||||
if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) {
|
||||
allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress)
|
||||
|
|
@ -497,13 +500,13 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
providerId = swapProvider.providerId,
|
||||
rateType = RateType.FLOAT,
|
||||
toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "",
|
||||
)
|
||||
).getOrNull()
|
||||
|
||||
val txData = walletManagersFacade.createTransaction(
|
||||
amount = amount.value.convertToAmount(currencyToSend.currency),
|
||||
fee = getFeeForTransaction(txFee),
|
||||
memo = null,
|
||||
destination = (exchangeData.dataModel?.transaction as ExpressTransactionModel.CEX).txTo,
|
||||
destination = (exchangeData?.transaction as ExpressTransactionModel.CEX).txTo,
|
||||
userWalletId = userWalletId,
|
||||
network = currencyToSend.currency.network,
|
||||
)
|
||||
|
|
@ -514,7 +517,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
network = currencyToSend.currency.network,
|
||||
)
|
||||
|
||||
val externalUrl = (exchangeData.dataModel.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl
|
||||
val externalUrl = (exchangeData.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl
|
||||
|
||||
return result.fold(
|
||||
ifLeft = {
|
||||
|
|
@ -533,7 +536,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
currencyToGet = currencyToGet,
|
||||
amount = amount,
|
||||
swapProvider = swapProvider,
|
||||
swapDataModel = exchangeData.dataModel,
|
||||
swapDataModel = exchangeData,
|
||||
timestamp = timestamp,
|
||||
)
|
||||
storeLastCryptoCurrencyId(currencyToGet.currency)
|
||||
|
|
@ -544,10 +547,10 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
),
|
||||
fromAmountValue = amount.value,
|
||||
toAmount = amountFormatter.formatSwapAmountToUI(
|
||||
exchangeData.dataModel.toTokenAmount,
|
||||
exchangeData.toTokenAmount,
|
||||
currencyToGet.currency.symbol,
|
||||
),
|
||||
toAmountValue = exchangeData.dataModel.toTokenAmount.value,
|
||||
toAmountValue = exchangeData.toTokenAmount.value,
|
||||
txAddress = userWalletManager.getLastTransactionHash(
|
||||
currencyToSend.currency.network.backendId,
|
||||
derivationPath,
|
||||
|
|
@ -751,7 +754,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
|
||||
private suspend fun getQuotesState(
|
||||
exchangeProviderType: ExchangeProviderType,
|
||||
quoteDataModel: AggregatedSwapDataModel<QuoteModel>,
|
||||
quoteDataModel: Either<DataError, QuoteModel>,
|
||||
amount: SwapAmount,
|
||||
fromToken: CryptoCurrencyStatus,
|
||||
toToken: CryptoCurrencyStatus,
|
||||
|
|
@ -761,58 +764,60 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
txFee: TxFeeState,
|
||||
includeFeeInAmount: IncludeFeeInAmount,
|
||||
): SwapState {
|
||||
val quoteModel = quoteDataModel.dataModel
|
||||
if (quoteModel != null) {
|
||||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
toTokenStatus = toToken,
|
||||
fromTokenAmount = amount,
|
||||
toTokenAmount = quoteModel.toTokenAmount,
|
||||
swapData = null,
|
||||
txFeeState = txFee,
|
||||
)
|
||||
return quoteDataModel.fold(
|
||||
ifRight = { quoteModel ->
|
||||
val swapState = updateBalances(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
toTokenStatus = toToken,
|
||||
fromTokenAmount = amount,
|
||||
toTokenAmount = quoteModel.toTokenAmount,
|
||||
swapData = null,
|
||||
txFeeState = txFee,
|
||||
)
|
||||
|
||||
return when (exchangeProviderType) {
|
||||
ExchangeProviderType.DEX -> {
|
||||
val state = updatePermissionState(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
swapAmount = amount,
|
||||
quotesLoadedState = swapState,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
spenderAddress = quoteModel.allowanceContract,
|
||||
)
|
||||
state.copy(
|
||||
preparedSwapConfigState = state.preparedSwapConfigState.copy(
|
||||
when (exchangeProviderType) {
|
||||
ExchangeProviderType.DEX -> {
|
||||
val state = updatePermissionState(
|
||||
networkId = networkId,
|
||||
fromTokenStatus = fromToken,
|
||||
swapAmount = amount,
|
||||
quotesLoadedState = swapState,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceEnough = isBalanceWithoutFeeEnough,
|
||||
),
|
||||
)
|
||||
spenderAddress = quoteModel.allowanceContract,
|
||||
)
|
||||
state.copy(
|
||||
preparedSwapConfigState = state.preparedSwapConfigState.copy(
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceEnough = isBalanceWithoutFeeEnough,
|
||||
),
|
||||
)
|
||||
}
|
||||
ExchangeProviderType.CEX -> {
|
||||
swapState.copy(
|
||||
permissionState = PermissionDataState.Empty,
|
||||
preparedSwapConfigState = PreparedSwapConfigState(
|
||||
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceEnough = isBalanceWithoutFeeEnough,
|
||||
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
|
||||
includeFeeInAmount = includeFeeInAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
ExchangeProviderType.CEX -> {
|
||||
swapState.copy(
|
||||
permissionState = PermissionDataState.Empty,
|
||||
preparedSwapConfigState = PreparedSwapConfigState(
|
||||
isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough,
|
||||
isAllowedToSpend = isAllowedToSpend,
|
||||
isBalanceEnough = isBalanceWithoutFeeEnough,
|
||||
hasOutgoingTransaction = hasOutgoingTransaction(fromToken),
|
||||
includeFeeInAmount = includeFeeInAmount,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val rates = getQuotes(fromToken.currency.id)
|
||||
val fromTokenSwapInfo = TokenSwapInfo(
|
||||
tokenAmount = amount,
|
||||
amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
|
||||
?: BigDecimal.ZERO,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
)
|
||||
return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error)
|
||||
}
|
||||
},
|
||||
ifLeft = { error ->
|
||||
val rates = getQuotes(fromToken.currency.id)
|
||||
val fromTokenSwapInfo = TokenSwapInfo(
|
||||
tokenAmount = amount,
|
||||
amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
|
||||
?: BigDecimal.ZERO,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
)
|
||||
return SwapState.SwapError(fromTokenSwapInfo, error)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getIncludeFeeInAmount(
|
||||
|
|
@ -882,7 +887,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
amount: SwapAmount,
|
||||
selectedFee: FeeType,
|
||||
): SwapState {
|
||||
repository.getExchangeData(
|
||||
return repository.getExchangeData(
|
||||
fromContractAddress = fromToken.currency.getContractAddress(),
|
||||
fromNetwork = fromToken.currency.network.backendId,
|
||||
toContractAddress = toToken.currency.getContractAddress(),
|
||||
|
|
@ -893,9 +898,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
providerId = provider.providerId,
|
||||
rateType = RateType.FLOAT,
|
||||
toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "",
|
||||
).let {
|
||||
val swapData = it.dataModel
|
||||
if (swapData != null) {
|
||||
).fold(
|
||||
ifRight = { swapData ->
|
||||
val feeData = transactionManager.getFee(
|
||||
networkId = networkId,
|
||||
amountToSend = amount.value,
|
||||
|
|
@ -926,7 +930,7 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
swapData = swapData,
|
||||
txFeeState = txFeeState,
|
||||
)
|
||||
return swapState.copy(
|
||||
swapState.copy(
|
||||
permissionState = PermissionDataState.Empty,
|
||||
preparedSwapConfigState = PreparedSwapConfigState(
|
||||
isAllowedToSpend = true,
|
||||
|
|
@ -936,7 +940,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex
|
||||
),
|
||||
)
|
||||
} else {
|
||||
},
|
||||
ifLeft = { error ->
|
||||
val rates = getQuotes(fromToken.currency.id)
|
||||
val fromTokenSwapInfo = TokenSwapInfo(
|
||||
tokenAmount = amount,
|
||||
|
|
@ -944,12 +949,12 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
?: BigDecimal.ZERO,
|
||||
cryptoCurrencyStatus = fromToken,
|
||||
)
|
||||
return SwapState.SwapError(
|
||||
SwapState.SwapError(
|
||||
fromTokenSwapInfo,
|
||||
it.error,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -999,9 +1004,8 @@ internal class SwapInteractorImpl @Inject constructor(
|
|||
networkId: String,
|
||||
): TxFeeState {
|
||||
getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId ->
|
||||
val txFeeResult = getFeeUseCase(
|
||||
val txFeeResult = estimateFeeUseCase(
|
||||
amount = amount.value,
|
||||
destination = fromToken.value.networkAddress?.defaultAddress?.value ?: "",
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = fromToken.currency,
|
||||
).firstOrNull()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade
|
|||
import com.tangem.domain.wallets.legacy.WalletsStateHolder
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.*
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.lib.crypto.TransactionManager
|
||||
import com.tangem.lib.crypto.UserWalletManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
package com.tangem.feature.swap.domain.models.data
|
||||
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
|
||||
/**
|
||||
* Model that aggregate data model from repository return with error [DataError] if it exists
|
||||
*
|
||||
* @param T model type
|
||||
* @property dataModel
|
||||
* @property error possible from repository [DataError]
|
||||
*/
|
||||
data class AggregatedSwapDataModel<T>(
|
||||
val dataModel: T?,
|
||||
val error: DataError = DataError.UnknownError,
|
||||
)
|
||||
|
|
@ -26,6 +26,10 @@ dependencies {
|
|||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
|
||||
/** AndroidX */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
|
@ -48,11 +52,6 @@ dependencies {
|
|||
implementation(projects.features.swap.api)
|
||||
implementation(projects.features.tokendetails.api)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.settings)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.compose.shimmer)
|
||||
implementation(deps.kotlin.serialization)
|
||||
|
|
|
|||
|
|
@ -651,9 +651,17 @@ internal class StateBuilder(
|
|||
}
|
||||
|
||||
fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
val warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }.toMutableList()
|
||||
warnings.add(
|
||||
0,
|
||||
SwapWarning.TransactionInProgressWarning(
|
||||
title = resourceReference(R.string.swapping_pending_transaction_title),
|
||||
description = resourceReference(R.string.swapping_pending_transaction_subtitle),
|
||||
),
|
||||
)
|
||||
return uiState.copy(
|
||||
permissionState = SwapPermissionState.InProgress,
|
||||
warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded },
|
||||
warnings = warnings,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ import com.tangem.feature.swap.domain.models.ExpressException
|
|||
import com.tangem.feature.swap.domain.models.domain.PermissionOptions
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapDataModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.feature.swap.router.SwapNavScreen
|
||||
|
|
@ -560,6 +560,7 @@ internal class SwapViewModel @Inject constructor(
|
|||
is TxState.TxSent -> {
|
||||
uiState = stateBuilder.loadingPermissionState(uiState)
|
||||
uiState = stateBuilder.dismissBottomSheet(uiState)
|
||||
startLoadingQuotesFromLastState(isSilent = true)
|
||||
}
|
||||
is TxState.UserCancelled -> Unit
|
||||
else -> {
|
||||
|
|
|
|||
|
|
@ -70,9 +70,10 @@ dependencies {
|
|||
|
||||
/** Temp dependency to swap domain */
|
||||
implementation(projects.features.swap.domain)
|
||||
implementation(projects.features.swap.domain.api)
|
||||
implementation(projects.features.swap.domain.models)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.tokendetails.api)
|
||||
implementation(projects.features.send.api)
|
||||
implementation(projects.features.swap.domain)
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.SwapProvider
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class SwapTransactionsState(
|
||||
val txId: String,
|
||||
|
|
@ -14,10 +15,10 @@ internal data class SwapTransactionsState(
|
|||
val txUrl: String? = null,
|
||||
val timestamp: TextReference,
|
||||
val fiatSymbol: String,
|
||||
val activeStatus: MutableStateFlow<ExchangeStatus?>,
|
||||
val hasFailed: MutableStateFlow<Boolean>,
|
||||
val statuses: MutableStateFlow<List<ExchangeStatusState>>,
|
||||
val notification: MutableStateFlow<ExchangeStatusNotifications?> = MutableStateFlow(null),
|
||||
val activeStatus: ExchangeStatus?,
|
||||
val hasFailed: Boolean,
|
||||
val statuses: ImmutableList<ExchangeStatusState>,
|
||||
val notification: ExchangeStatusNotifications? = null,
|
||||
val toCryptoCurrencyId: CryptoCurrency.ID,
|
||||
val toCryptoAmount: String,
|
||||
val toCryptoSymbol: String,
|
||||
|
|
@ -32,6 +33,7 @@ internal data class SwapTransactionsState(
|
|||
val onGoToProviderClick: (String) -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal class ExchangeStatusState(
|
||||
val status: ExchangeStatus,
|
||||
val text: TextReference,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,23 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) {
|
|||
),
|
||||
)
|
||||
|
||||
data class SwapPromo(
|
||||
val onSwapClick: () -> Unit,
|
||||
val onCloseClick: () -> Unit,
|
||||
) : TokenDetailsNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(id = R.string.token_swap_promotion_title),
|
||||
subtitle = resourceReference(id = R.string.token_swap_promotion_message),
|
||||
iconResId = R.drawable.ic_swap_promo_34,
|
||||
backgroundResId = R.drawable.img_swap_promo_banner_background,
|
||||
onCloseClick = onCloseClick,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button),
|
||||
onClick = onSwapClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
object NetworksUnreachable : Warning(
|
||||
title = resourceReference(R.string.warning_network_unreachable_title),
|
||||
subtitle = resourceReference(R.string.warning_network_unreachable_message),
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ internal class TokenDetailsNotificationConverter(
|
|||
is CryptoCurrencyWarning.HasPendingTransactions -> TokenDetailsNotification.HasPendingTransactions(
|
||||
coinSymbol = warning.blockchainSymbol,
|
||||
)
|
||||
is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo(
|
||||
onSwapClick = clickIntents::onSwapPromoClick,
|
||||
onCloseClick = clickIntents::onSwapPromoDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
internal class TokenDetailsStateFactory(
|
||||
private val currentStateProvider: Provider<TokenDetailsState>,
|
||||
private val appCurrencyProvider: Provider<AppCurrency>,
|
||||
|
|
@ -247,6 +248,19 @@ internal class TokenDetailsStateFactory(
|
|||
)
|
||||
}
|
||||
|
||||
fun updateStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TangemBottomSheetConfig? {
|
||||
val state = currentStateProvider()
|
||||
val bottomSheetConfig = state.bottomSheetConfig
|
||||
val currentConfig = bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return bottomSheetConfig
|
||||
return bottomSheetConfig.copy(
|
||||
content = if (currentConfig.value != swapTxState) {
|
||||
ExchangeStatusBottomSheetConfig(swapTxState)
|
||||
} else {
|
||||
currentConfig
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun getStateAndTriggerEvent(
|
||||
state: TokenDetailsState,
|
||||
errorMessage: TextReference,
|
||||
|
|
|
|||
|
|
@ -20,11 +20,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.PersistentList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class TokenDetailsSwapTransactionsStateConverter(
|
||||
|
|
@ -70,14 +69,12 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
txUrl = transaction.status?.txUrl,
|
||||
timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
|
||||
fiatSymbol = appCurrency.symbol,
|
||||
statuses = MutableStateFlow(getStatuses(transaction.status?.status)),
|
||||
hasFailed = MutableStateFlow(transaction.status?.status == ExchangeStatus.Failed),
|
||||
activeStatus = MutableStateFlow(transaction.status?.status),
|
||||
notification = MutableStateFlow(
|
||||
getNotification(
|
||||
transaction.status?.status,
|
||||
transaction.status?.txUrl,
|
||||
),
|
||||
statuses = getStatuses(transaction.status?.status),
|
||||
hasFailed = transaction.status?.status == ExchangeStatus.Failed,
|
||||
activeStatus = transaction.status?.status,
|
||||
notification = getNotification(
|
||||
transaction.status?.status,
|
||||
transaction.status?.txUrl,
|
||||
),
|
||||
toCryptoCurrencyId = toCurrency.currency.id,
|
||||
toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(
|
||||
|
|
@ -109,13 +106,16 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
return result.toPersistentList()
|
||||
}
|
||||
|
||||
fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?) {
|
||||
if (statusModel == null || tx.activeStatus.value == statusModel.status) return
|
||||
val hasFailed = tx.hasFailed.value || statusModel.status == ExchangeStatus.Failed
|
||||
tx.activeStatus.update { statusModel.status }
|
||||
tx.hasFailed.update { hasFailed }
|
||||
tx.notification.update { getNotification(statusModel.status, statusModel.txUrl) }
|
||||
tx.statuses.update { getStatuses(statusModel.status, hasFailed) }
|
||||
fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState {
|
||||
if (statusModel == null || tx.activeStatus == statusModel.status) return tx
|
||||
val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed
|
||||
return tx.copy(
|
||||
activeStatus = statusModel.status,
|
||||
hasFailed = hasFailed,
|
||||
notification = getNotification(statusModel.status, statusModel.txUrl),
|
||||
statuses = getStatuses(statusModel.status, hasFailed),
|
||||
txUrl = statusModel.txUrl,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getFiatAmount(toFiatAmount: BigDecimal): String {
|
||||
|
|
@ -149,8 +149,8 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): List<ExchangeStatusState> {
|
||||
if (status == null) return emptyList()
|
||||
private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList<ExchangeStatusState> {
|
||||
if (status == null) return persistentListOf()
|
||||
val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting
|
||||
val isConfirming = status == ExchangeStatus.Confirming
|
||||
val isVerifying = status == ExchangeStatus.Verifying
|
||||
|
|
@ -181,7 +181,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
|
|||
isRefunded = isRefunded,
|
||||
hasFailed = hasFailed,
|
||||
),
|
||||
)
|
||||
).toPersistentList()
|
||||
}
|
||||
|
||||
private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto
|
|||
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
||||
import com.tangem.core.ui.components.transactions.state.TxHistoryState
|
||||
import com.tangem.core.ui.components.transactions.txHistoryItems
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
|
|
@ -104,14 +105,21 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
|
|||
key = { it::class.java },
|
||||
contentType = { it.config::class.java },
|
||||
itemContent = {
|
||||
Notification(
|
||||
modifier = itemModifier.animateItemPlacement(),
|
||||
config = it.config,
|
||||
iconTint = when (it) {
|
||||
is TokenDetailsNotification.Warning -> null
|
||||
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
},
|
||||
)
|
||||
if (it is TokenDetailsNotification.SwapPromo) {
|
||||
NotificationWithBackground(
|
||||
config = it.config,
|
||||
modifier = itemModifier.animateItemPlacement(),
|
||||
)
|
||||
} else {
|
||||
Notification(
|
||||
modifier = itemModifier.animateItemPlacement(),
|
||||
config = it.config,
|
||||
iconTint = when (it) {
|
||||
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (state.isMarketPriceAvailable) {
|
||||
|
|
|
|||
|
|
@ -18,23 +18,21 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun ExchangeStatusBlock(
|
||||
statuses: MutableStateFlow<List<ExchangeStatusState>>,
|
||||
statuses: ImmutableList<ExchangeStatusState>,
|
||||
showLink: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val statusValues = statuses.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
|
|
@ -77,10 +75,10 @@ internal fun ExchangeStatusBlock(
|
|||
}
|
||||
}
|
||||
|
||||
statusValues.value.forEachIndexed { index, item ->
|
||||
statuses.forEachIndexed { index, item ->
|
||||
ExchangeStatusStep(
|
||||
stepStatus = item,
|
||||
isLast = index == statusValues.value.lastIndex,
|
||||
isLast = index == statuses.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH10
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
|
|
@ -36,8 +35,6 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
|
|||
@Composable
|
||||
private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) {
|
||||
val config = content.value
|
||||
val status = config.activeStatus.collectAsStateWithLifecycle()
|
||||
val notification = config.notification.collectAsStateWithLifecycle()
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
|
|
@ -77,15 +74,15 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC
|
|||
SpacerH12()
|
||||
ExchangeStatusBlock(
|
||||
statuses = config.statuses,
|
||||
showLink = notification.value == null && config.txUrl != null,
|
||||
showLink = config.notification == null && config.txUrl != null,
|
||||
onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) },
|
||||
)
|
||||
AnimatedContent(
|
||||
targetState = notification.value,
|
||||
targetState = config.notification,
|
||||
label = "Exchange Status Notification Change",
|
||||
) {
|
||||
it?.let {
|
||||
val tint = when (status.value) {
|
||||
val tint = when (config.activeStatus) {
|
||||
ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention
|
||||
ExchangeStatus.Failed -> TangemTheme.colors.icon.warning
|
||||
else -> null
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.constraintlayout.compose.*
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
|
|
@ -41,8 +40,7 @@ internal fun LazyListScope.swapTransactionsItems(
|
|||
contentType = { swapTxs[it]::class.java },
|
||||
) {
|
||||
val item = swapTxs[it]
|
||||
val status = item.activeStatus.collectAsStateWithLifecycle()
|
||||
val (iconRes, tint) = when (status.value) {
|
||||
val (iconRes, tint) = when (item.activeStatus) {
|
||||
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
|
||||
ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
|
||||
else -> null to null
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
|||
import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
|
||||
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
|
||||
import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel
|
||||
|
|
@ -81,8 +81,9 @@ internal class ExchangeStatusFactory(
|
|||
swapTxList.map { tx ->
|
||||
async {
|
||||
val statusModel = getExchangeStatus(tx.txId)
|
||||
swapTransactionsStateConverter.updateTxStatus(tx, statusModel)
|
||||
tx.removeIfFinished(statusModel?.status)
|
||||
swapTransactionsStateConverter
|
||||
.updateTxStatus(tx, statusModel)
|
||||
.removeIfFinished(statusModel?.status)
|
||||
}
|
||||
}
|
||||
.awaitAll()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels
|
|||
import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface TokenDetailsClickIntents {
|
||||
|
||||
fun onBackClick()
|
||||
|
|
@ -42,4 +43,8 @@ interface TokenDetailsClickIntents {
|
|||
fun onSwapTransactionClick(txId: String)
|
||||
|
||||
fun onGoToProviderClick(url: String)
|
||||
|
||||
fun onSwapPromoDismiss()
|
||||
|
||||
fun onSwapPromoClick()
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
|||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -28,6 +29,7 @@ import com.tangem.domain.tokens.model.NetworkAddress
|
|||
import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
|
||||
import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent
|
||||
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase
|
||||
import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase
|
||||
|
|
@ -35,12 +37,13 @@ import com.tangem.domain.wallets.models.UserWalletId
|
|||
import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.feature.swap.domain.SwapRepository
|
||||
import com.tangem.feature.swap.domain.SwapTransactionRepository
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
|
||||
import com.tangem.utils.coroutines.*
|
||||
|
|
@ -74,6 +77,7 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase,
|
||||
private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
|
||||
private val swapRepository: SwapRepository,
|
||||
private val swapTransactionRepository: SwapTransactionRepository,
|
||||
private val swapTransactionStatusStore: SwapTransactionStatusStore,
|
||||
|
|
@ -226,7 +230,14 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
}
|
||||
},
|
||||
onSuccess = { updatedTxs ->
|
||||
uiState = uiState.copy(swapTxs = updatedTxs)
|
||||
val config = uiState.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig
|
||||
val currentTx = updatedTxs.firstOrNull { it.txId == config?.value?.txId }
|
||||
uiState = uiState.copy(
|
||||
swapTxs = updatedTxs,
|
||||
bottomSheetConfig = currentTx?.let(
|
||||
stateFactory::updateStateWithExchangeStatusBottomSheet,
|
||||
),
|
||||
)
|
||||
},
|
||||
onError = {},
|
||||
),
|
||||
|
|
@ -547,6 +558,18 @@ internal class TokenDetailsViewModel @Inject constructor(
|
|||
router.openUrl(url)
|
||||
}
|
||||
|
||||
override fun onSwapPromoDismiss() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldShowSwapPromoTokenUseCase.neverToShow()
|
||||
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSwapPromoClick() {
|
||||
onSwapClick()
|
||||
analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,4 +166,16 @@ sealed class WalletNotification(val config: NotificationConfig) {
|
|||
onCloseClick = onCloseClick,
|
||||
),
|
||||
)
|
||||
|
||||
data class SwapPromo(
|
||||
val onCloseClick: () -> Unit,
|
||||
) : WalletNotification(
|
||||
config = NotificationConfig(
|
||||
title = resourceReference(id = R.string.main_swap_promotion_title),
|
||||
subtitle = resourceReference(id = R.string.main_swap_promotion_message),
|
||||
iconResId = R.drawable.ic_swap_promo_34,
|
||||
backgroundResId = R.drawable.img_swap_promo_banner_background,
|
||||
onCloseClick = onCloseClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope
|
|||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.notifications.Notification
|
||||
import com.tangem.core.ui.components.notifications.NotificationWithBackground
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -24,17 +25,24 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
|
|||
key = { it::class.java },
|
||||
contentType = { it::class.java },
|
||||
itemContent = {
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
iconTint = when (it) {
|
||||
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
|
||||
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
|
||||
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
|
||||
is WalletNotification.Warning -> null
|
||||
},
|
||||
)
|
||||
if (it is WalletNotification.SwapPromo) {
|
||||
NotificationWithBackground(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
)
|
||||
} else {
|
||||
Notification(
|
||||
config = it.config,
|
||||
modifier = modifier.animateItemPlacement(),
|
||||
iconTint = when (it) {
|
||||
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
|
||||
is WalletNotification.Informational -> TangemTheme.colors.icon.accent
|
||||
is WalletNotification.RateApp -> TangemTheme.colors.icon.attention
|
||||
is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -72,4 +72,6 @@ internal interface WalletClickIntents {
|
|||
fun onExploreClick()
|
||||
|
||||
fun onTransactionClick(txHash: String)
|
||||
|
||||
fun onCloseSwapPromoNotificationClick()
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import com.tangem.domain.common.CardTypesResolver
|
|||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
|
||||
import com.tangem.domain.tokens.GetMissedAddressesCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.error.GetCurrenciesError
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
|
|
@ -26,16 +27,21 @@ import kotlinx.coroutines.flow.flowOf
|
|||
* @property isDemoCardUseCase use case that checks if card is demo
|
||||
* @property isReadyToShowRateAppUseCase use case that checks if card is user already rate app
|
||||
* @property isNeedToBackupUseCase use case that checks if wallet need backup cards
|
||||
* @property getMissedAddressCryptoCurrenciesUseCase use case that gets missed address crypto currencies
|
||||
* @property hasSingleWalletSignedHashesUseCase use case that checks if single wallet signed hashes
|
||||
* @property shouldShowSwapPromoWalletUseCase use case that checks if should show swap promo
|
||||
* @property clickIntents screen click intents
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
internal class WalletNotificationsListFactory(
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
private val getMissedAddressCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase,
|
||||
private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase,
|
||||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||
private val clickIntents: WalletClickIntents,
|
||||
) {
|
||||
|
||||
|
|
@ -51,9 +57,12 @@ internal class WalletNotificationsListFactory(
|
|||
flow2 = isReadyToShowRateAppUseCase().conflate(),
|
||||
flow3 = isNeedToBackupUseCase(selectedWallet.walletId).conflate(),
|
||||
flow4 = getMissedAddressCryptoCurrenciesUseCase(selectedWallet.walletId).conflate(),
|
||||
) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies ->
|
||||
flow5 = shouldShowSwapPromoWalletUseCase().conflate(),
|
||||
) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies, isShowSwapPromo ->
|
||||
readyForRateAppNotification = true
|
||||
buildList {
|
||||
addSwapPromoNotification(isShowSwapPromo, cardTypesResolver)
|
||||
|
||||
addCriticalNotifications(cardTypesResolver)
|
||||
|
||||
addInformationalNotifications(cardTypesResolver, maybeMissedAddressCurrencies)
|
||||
|
|
@ -77,6 +86,18 @@ internal class WalletNotificationsListFactory(
|
|||
}
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addSwapPromoNotification(
|
||||
showSwapPromo: Boolean,
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.SwapPromo(
|
||||
clickIntents::onCloseSwapPromoNotificationClick,
|
||||
),
|
||||
condition = showSwapPromo && cardTypesResolver.isMultiwalletAllowed(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
|
||||
addIf(
|
||||
element = WalletNotification.Critical.DevCard,
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler,
|
||||
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
|
||||
isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase,
|
||||
isNeedToBackupUseCase: IsNeedToBackupUseCase,
|
||||
getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase,
|
||||
|
|
@ -146,6 +147,7 @@ internal class WalletViewModel @Inject constructor(
|
|||
isNeedToBackupUseCase = isNeedToBackupUseCase,
|
||||
getMissedAddressCryptoCurrenciesUseCase = getMissedAddressesCryptoCurrenciesUseCase,
|
||||
hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase,
|
||||
shouldShowSwapPromoWalletUseCase = shouldShowSwapPromoWalletUseCase,
|
||||
clickIntents = this,
|
||||
)
|
||||
|
||||
|
|
@ -774,6 +776,12 @@ internal class WalletViewModel @Inject constructor(
|
|||
refreshSingleCurrencyContent(selectedWalletIndex)
|
||||
}
|
||||
|
||||
override fun onCloseSwapPromoNotificationClick() {
|
||||
viewModelScope.launch(dispatchers.main) {
|
||||
shouldShowSwapPromoWalletUseCase.neverToShow()
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary
|
||||
// currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged()
|
||||
private fun refreshSingleCurrencyContent(walletIndex: Int) {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ spr-client = "3.6.2"
|
|||
# endregion Other libraries
|
||||
|
||||
# region Tangem
|
||||
tangemBlockchainSdk = "release-app_5.4-416"
|
||||
tangemBlockchainSdk = "release-app_5.4-417"
|
||||
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
|
||||
tangemCardSdk = "release-app_5.4-315"
|
||||
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^
|
||||
|
|
|
|||
|
|
@ -79,12 +79,16 @@ include(":libs:auth")
|
|||
|
||||
// region Feature modules
|
||||
include(":features:onboarding")
|
||||
|
||||
include(":features:referral:data")
|
||||
include(":features:referral:domain")
|
||||
include(":features:referral:presentation")
|
||||
|
||||
include(":features:swap:api")
|
||||
include(":features:swap:data")
|
||||
include(":features:swap:domain")
|
||||
include(":features:swap:domain:models")
|
||||
include(":features:swap:domain:api")
|
||||
include(":features:swap:presentation")
|
||||
|
||||
include(":features:tester:api")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue