Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-16 19:36:26 +01:00
parent 2ff236ef2f
commit 262f8082bb
12 changed files with 138 additions and 35 deletions

View file

@ -28,6 +28,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.fiat
@ -152,6 +153,7 @@ internal class SwapModel @Inject constructor(
private val swapTransferInteractor: SwapTransferInteractor,
private val swapTransferStateBuilder: SwapTransferStateBuilder,
private val urlOpener: UrlOpener,
private val shareManager: ShareManager,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
private val tangemPayWithdrawWithSwapUseCase: TangemPayWithdrawWithSwapUseCase,
@ -784,6 +786,7 @@ internal class SwapModel @Inject constructor(
feePaidCurrencyStatus = feePaidCryptoCurrency,
fee = selectedFee,
)
uiState = swapTransferStateBuilder.updateTransferTitle(uiState)
when (swapState) {
is SwapState.EmptyAmountState -> setupEmptyAmountUiState(swapState, fromSwapCurrencyStatus)
is SwapState.Transfer -> {
@ -1454,6 +1457,12 @@ internal class SwapModel @Inject constructor(
urlOpener.openUrl(txUrl)
}
},
onShareClick = {
val txUrl = uiState.successState?.txUrl.orEmpty()
if (txUrl.isNotEmpty()) {
shareManager.shareText(txUrl)
}
},
)
router.replaceAll(SwapRoute.Success)
}
@ -1499,6 +1508,11 @@ internal class SwapModel @Inject constructor(
urlOpener.openUrl(txUrl)
}
},
onShareClick = {
if (txUrl.isNotEmpty()) {
shareManager.shareText(txUrl)
}
},
)
router.replaceAll(SwapRoute.Success)
},

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
@ -17,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class SwapStateHolder(
@param:StringRes val titleId: Int,
val sendCardData: SwapCardState,
val receiveCardData: SwapCardState,
val notifications: ImmutableList<NotificationUM> = persistentListOf(),

View file

@ -1,6 +1,7 @@
package com.tangem.feature.swap.models
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
@ -22,7 +23,7 @@ data class SwapSuccessStateHolder(
val toTokenFiatAmount: TextReference,
val fromTokenIconState: CurrencyIconState?,
val toTokenIconState: CurrencyIconState?,
val onExploreButtonClick: () -> Unit,
val navigationUM: NavigationUM,
val onStatusButtonClick: () -> Unit,
) {
val shouldShowProvider: Boolean

View file

@ -9,6 +9,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.ui.swapSuccessNavigation
internal data object SwapSuccessStatePreview {
val state = SwapSuccessStateHolder(
@ -37,7 +38,7 @@ internal data object SwapSuccessStatePreview {
fromTokenIconState = CurrencyIconState.Loading,
toTokenIconState = CurrencyIconState.Loading,
rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
onExploreButtonClick = {},
navigationUM = swapSuccessNavigation(txUrl = "https://www.google.com/#q=nam", exploreClick = {}),
onStatusButtonClick = {},
)
}

View file

@ -91,6 +91,7 @@ internal class StateBuilder(
fun createInitialLoadingState(swapUIMode: SwapUIMode = SwapUIMode.Detailed): SwapStateHolder {
return SwapStateHolder(
titleId = R.string.common_swap,
sendCardData = getEmptyCardState(
isFromCard = true,
emptyAmountState = SwapState.EmptyAmountState(TextReference.EMPTY),
@ -205,6 +206,7 @@ internal class StateBuilder(
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
return uiStateHolder.copy(
titleId = R.string.common_swap,
sendCardData = uiStateHolder.sendCardData.copy(
type = TransactionCardType.Inputtable(
onCurrencyChange = actions.onCurrencyChange,
@ -1093,7 +1095,7 @@ internal class StateBuilder(
toTokenFiatAmount = toFiatAmount,
fromTokenIconState = iconStateConverter.convert(fromSwapCurrencyStatus.status),
toTokenIconState = iconStateConverter.convert(toSwapCurrencyStatus.status),
onExploreButtonClick = onExploreClick,
navigationUM = swapSuccessNavigation(txUrl = txUrl, exploreClick = onExploreClick),
onStatusButtonClick = onStatusClick,
),
)
@ -1134,7 +1136,7 @@ internal class StateBuilder(
toTokenFiatAmount = toFiatAmount,
fromTokenIconState = iconStateConverter.convert(fromSwapCurrencyStatus.status),
toTokenIconState = iconStateConverter.convert(toSwapCurrencyStatus.status),
onExploreButtonClick = onExploreClick,
navigationUM = swapSuccessNavigation(txUrl = txUrl, exploreClick = onExploreClick),
onStatusButtonClick = {},
),
)

View file

@ -83,7 +83,7 @@ private fun SwapTopBar(stateHolder: SwapStateHolder) {
var shouldShowModeMenu by rememberSaveable { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxWidth()) {
AppBarWithBackButtonAndIcon(
text = stringResourceSafe(R.string.common_swap),
text = stringResourceSafe(stateHolder.titleId),
backIconRes = R.drawable.ic_close_24,
iconRes = if (stateHolder.shouldShowAbMenu) R.drawable.ic_more_vertical_24 else null,
onIconClick = if (stateHolder.shouldShowAbMenu) {

View file

@ -413,6 +413,7 @@ private fun getButtonTitle(mode: SwapButton.Mode): String {
// region preview
private val state = SwapStateHolder(
titleId = R.string.common_swap,
sendCardData = sendCard,
receiveCardData = receiveCard,
notifications = persistentListOf(

View file

@ -0,0 +1,49 @@
package com.tangem.feature.swap.ui
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.swap.presentation.R
/**
* Builds the [NavigationUM] for the swap success screen, mirroring
* `SendConfirmSuccessModel.configConfirmSuccessNavigation`.
*
* The explore button is always the single [NavigationUM.Content.primaryButton]; a transfer also
* gets a share button, surfaced together as [NavigationUM.Content.secondaryPairButtonsUM] (the same
* explore + share pair the send flow shows). [shareClick] is `null` for a regular swap, so the pair
* is absent and only the explore button is rendered.
*/
internal fun swapSuccessNavigation(
txUrl: String,
exploreClick: () -> Unit,
shareClick: (() -> Unit)? = null,
): NavigationUM {
val exploreButton = NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_web_24,
onClick = exploreClick,
)
val pairButtons = shareClick
?.takeIf { txUrl.isNotEmpty() }
?.let { onShare ->
exploreButton to NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
onClick = onShare,
)
}
return NavigationUM.Content(
source = SOURCE,
title = TextReference.EMPTY,
subtitle = null,
backIconRes = R.drawable.ic_close_24,
backIconClick = {},
primaryButton = exploreButton,
secondaryPairButtonsUM = pairButtons,
)
}
private const val SOURCE = "SwapSuccess"

View file

@ -14,6 +14,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.navigationButtons.DoneButtons
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
@ -49,10 +51,7 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, feeSelectorUM: FeeSelectorU
bottomBar = {
SwapSuccessScreenButtons(
textRes = R.string.common_close,
txUrl = state.txUrl,
shouldShowStatusButton = state.shouldShowStatusButton,
onExploreClick = state.onExploreButtonClick,
onStatusClick = state.onStatusButtonClick,
state = state,
onDoneClick = onBack,
)
},
@ -170,40 +169,40 @@ private fun SwapAmountBlock(
}
}
@Suppress("LongParameterList")
@Composable
private fun SwapSuccessScreenButtons(
@StringRes textRes: Int,
txUrl: String,
shouldShowStatusButton: Boolean,
onExploreClick: () -> Unit,
onStatusClick: () -> Unit,
onDoneClick: () -> Unit,
) {
private fun SwapSuccessScreenButtons(@StringRes textRes: Int, state: SwapSuccessStateHolder, onDoneClick: () -> Unit) {
val content = state.navigationUM as? NavigationUM.Content
Column(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(TangemTheme.dimens.spacing16),
) {
if (txUrl.isNotBlank()) {
Row {
SecondaryButtonIconStart(
text = stringResourceSafe(id = R.string.common_explore),
iconResId = R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
if (shouldShowStatusButton) {
SpacerW12()
val pairButtons = content?.secondaryPairButtonsUM
when {
// Transfer mode: explore + share, the same pair the send success screen shows.
pairButtons != null -> DoneButtons(pairButtons)
// Regular swap: single explore button (+ provider status for CEX).
content != null && state.txUrl.isNotBlank() -> {
Row {
val exploreButton = content.primaryButton
SecondaryButtonIconStart(
text = stringResourceSafe(id = R.string.express_provider),
iconResId = R.drawable.ic_arrow_top_right_24,
onClick = onStatusClick,
text = exploreButton.textReference.resolveReference(),
iconResId = requireNotNull(exploreButton.iconRes),
onClick = exploreButton.onClick,
modifier = Modifier.weight(1f),
)
if (state.shouldShowStatusButton) {
SpacerW12()
SecondaryButtonIconStart(
text = stringResourceSafe(id = R.string.express_provider),
iconResId = R.drawable.ic_arrow_top_right_24,
onClick = state.onStatusButtonClick,
modifier = Modifier.weight(1f),
)
}
}
SpacerH12()
}
SpacerH12()
}
PrimaryButton(
text = stringResourceSafe(id = textRes),

View file

@ -32,6 +32,7 @@ import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.feature.swap.model.SwapProcessDataState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.ui.SwapAmountScreenClickIntents
import com.tangem.feature.swap.ui.swapSuccessNavigation
import com.tangem.feature.swap.models.SwapButton.Mode
import com.tangem.feature.swap.models.states.SwapNotificationUM
import com.tangem.feature.swap.presentation.R
@ -420,6 +421,7 @@ internal class SwapTransferStateBuilder @Inject constructor(
txUrl: String,
timestamp: Long,
onExplorerClick: () -> Unit,
onShareClick: () -> Unit,
): SwapStateHolder {
val transferState = requireNotNull(dataState.currentTransferState)
val fromSwapCurrencyStatus = requireNotNull(dataState.fromSwapCurrencyStatus)
@ -466,7 +468,11 @@ internal class SwapTransferStateBuilder @Inject constructor(
toTokenFiatAmount = toFiatAmount,
fromTokenIconState = iconConverter.convert(fromSwapCurrencyStatus.status),
toTokenIconState = iconConverter.convert(toSwapCurrencyStatus.status),
onExploreButtonClick = onExplorerClick,
navigationUM = swapSuccessNavigation(
txUrl = txUrl,
exploreClick = onExplorerClick,
shareClick = onShareClick,
),
onStatusButtonClick = {},
),
)
@ -477,6 +483,7 @@ internal class SwapTransferStateBuilder @Inject constructor(
dataState: SwapProcessDataState,
fee: Fee?,
onExploreClick: () -> Unit,
onShareClick: () -> Unit,
): SwapStateHolder {
val fromSwapCurrencyStatus = requireNotNull(dataState.fromSwapCurrencyStatus)
val toSwapCurrencyStatus = requireNotNull(dataState.toSwapCurrencyStatus)
@ -515,7 +522,11 @@ internal class SwapTransferStateBuilder @Inject constructor(
toTokenFiatAmount = fiatAmount,
fromTokenIconState = iconConverter.convert(fromSwapCurrencyStatus.status),
toTokenIconState = iconConverter.convert(toSwapCurrencyStatus.status),
onExploreButtonClick = onExploreClick,
navigationUM = swapSuccessNavigation(
txUrl = "",
exploreClick = onExploreClick,
shareClick = onShareClick,
),
onStatusButtonClick = {},
),
)
@ -535,4 +546,8 @@ internal class SwapTransferStateBuilder @Inject constructor(
}
return stringReference("$cryptoFormatted ($fiatFormatted)")
}
fun updateTransferTitle(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(titleId = R.string.common_transfer)
}
}

View file

@ -7,6 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.MutableParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.common.routing.AppRouter
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
@ -84,6 +85,7 @@ internal abstract class SwapModelTestBase {
protected val swapTransferInteractor: SwapTransferInteractor = mockk(relaxed = true)
protected val swapTransferStateBuilder: SwapTransferStateBuilder = mockk(relaxed = true)
protected val urlOpener: UrlOpener = mockk(relaxed = true)
protected val shareManager: ShareManager = mockk(relaxed = true)
protected val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase = mockk(relaxed = true)
protected val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase =
mockk(relaxed = true)
@ -154,6 +156,7 @@ internal abstract class SwapModelTestBase {
swapTransferInteractor = swapTransferInteractor,
swapTransferStateBuilder = swapTransferStateBuilder,
urlOpener = urlOpener,
shareManager = shareManager,
getAccountCurrencyStatusUseCase = getAccountCurrencyStatusUseCase,
getPaymentAccountCryptoCurrencyStatusUseCase = getPaymentAccountCryptoCurrencyStatusUseCase,
tangemPayWithdrawWithSwapUseCase = tangemPayWithdrawWithSwapUseCase,

View file

@ -12,6 +12,7 @@ import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.common.ui.navigationButtons.NavigationUM
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
@ -539,6 +540,7 @@ internal class SwapTransferStateBuilderTest {
txUrl = txUrl,
timestamp = timestamp,
onExplorerClick = {},
onShareClick = {},
)
val success = requireNotNull(result.successState)
@ -666,6 +668,7 @@ internal class SwapTransferStateBuilderTest {
dataState = dataState,
fee = null,
onExploreClick = onExploreClick,
onShareClick = {},
)
val after = System.currentTimeMillis()
@ -686,7 +689,7 @@ internal class SwapTransferStateBuilderTest {
assertThat(success.toTokenFiatAmount).isEqualTo(expectedFiat)
assertThat(success.fromTokenIconState).isEqualTo(fromIcon)
assertThat(success.toTokenIconState).isEqualTo(toIcon)
assertThat(success.onExploreButtonClick).isEqualTo(onExploreClick)
assertThat((success.navigationUM as NavigationUM.Content).primaryButton.onClick).isEqualTo(onExploreClick)
val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio
val expectedIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
@ -769,7 +772,20 @@ internal class SwapTransferStateBuilderTest {
)
}
@Test
fun `GIVEN swap title state WHEN updateTransferTitle THEN only titleId becomes transfer`() {
// Arrange
val uiState = baseStateHolder()
// Act
val result = sut.updateTransferTitle(uiState)
// Assert
assertThat(result).isEqualTo(uiState.copy(titleId = R.string.common_transfer))
}
private fun baseStateHolder(): SwapStateHolder = SwapStateHolder(
titleId = R.string.common_swap,
sendCardData = SwapCardState.SwapCardData(
appCurrency = AppCurrency.Default,
type = TransactionCardType.Inputtable(