Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-01 14:18:28 +02:00
parent 8d6992ad24
commit 646fe2d349
13 changed files with 72 additions and 32 deletions

View file

@ -17,7 +17,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withLink
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntSize
@ -26,8 +28,8 @@ import com.tangem.common.ui.R
import com.tangem.common.ui.bottomsheet.permission.state.*
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.extensions.*
@ -114,7 +116,22 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
@Composable
private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) {
FooterContainer(
footer = resourceReference(R.string.give_permission_policy_type_footer),
footer = annotatedReference {
append(stringResourceSafe(R.string.swap_approve_description))
append(" ")
withLink(
link = LinkAnnotation.Clickable(
tag = "APPROVE_TAG",
linkInteractionListener = { data.onOpenLearnMoreAboutApproveClick() },
),
block = {
appendColored(
text = stringResourceSafe(R.string.common_learn_more),
color = TangemTheme.colors.text.accent,
)
},
)
},
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
AmountItem(
@ -318,6 +335,7 @@ private val previewData = GiveTxPermissionBottomSheetConfig(
subtitle = resourceReference(R.string.give_permission_staking_subtitle, wrappedList("1")),
dialogText = resourceReference(R.string.give_permission_staking_footer),
footerText = resourceReference(R.string.swap_give_permission_fee_footer),
onOpenLearnMoreAboutApproveClick = {},
),
walletInteractionIcon = R.drawable.ic_tangem_24,
onCancel = {},

View file

@ -26,6 +26,7 @@ sealed class GiveTxPermissionState {
val approveButton: ApprovePermissionButton,
val cancelButton: CancelPermissionButton,
val onChangeApproveType: ((ApproveType) -> Unit)? = null,
val onOpenLearnMoreAboutApproveClick: () -> Unit,
) : GiveTxPermissionState()
fun GiveTxPermissionState.getApproveTypeOrNull(): ApproveType? {

View file

@ -18,4 +18,6 @@ object TangemLinks {
append("/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/")
}
}
const val RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP = "https://tangem.com/en/blog/post/give-revoke-permission/"
}

View file

@ -67,4 +67,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openTokenDetails(cryptoCurrency: CryptoCurrency)
fun showPrimaryClickAlert()
fun onOpenLearnMoreAboutApproveClick()
}

View file

@ -16,6 +16,7 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -73,6 +74,7 @@ import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalcula
import com.tangem.features.staking.impl.presentation.state.utils.isSingleAction
import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstakeAction
import com.tangem.utils.Provider
import com.tangem.utils.TangemLinks.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.isSingleItem
import com.tangem.utils.extensions.orZero
@ -123,6 +125,7 @@ internal class StakingModel @Inject constructor(
private val getActionRequirementAmountUseCase: GetActionRequirementAmountUseCase,
private val paramsInterceptorHolder: ParamsInterceptorHolder,
private val shareManager: ShareManager,
private val urlOpener: UrlOpener,
@DelayedWork private val coroutineScope: CoroutineScope,
private val innerRouter: InnerStakingRouter,
appRouter: AppRouter,
@ -927,6 +930,10 @@ internal class StakingModel @Inject constructor(
)
}
override fun onOpenLearnMoreAboutApproveClick() {
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
}
private suspend fun setupApprovalNeeded() {
val approval = StakingIntegrationID.create(currencyId = cryptoCurrencyStatus.currency.id)?.approval
?: StakingApproval.Empty

View file

@ -76,4 +76,6 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onActiveStakeAnalytic() {}
override fun showPrimaryClickAlert() {}
override fun onOpenLearnMoreAboutApproveClick() {}
}

View file

@ -77,6 +77,7 @@ internal class ShowApprovalBottomSheetTransformer(
dialogText = resourceReference(R.string.give_permission_staking_footer),
footerText = resourceReference(R.string.staking_give_permission_fee_footer),
onChangeApproveType = prevState.clickIntents::onApproveTypeChange,
onOpenLearnMoreAboutApproveClick = {},
),
walletInteractionIcon = walletInterationIcon(userWallet),
onCancel = onDismiss,

View file

@ -63,6 +63,7 @@ import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.features.swap.SwapComponent
import com.tangem.utils.Provider
import com.tangem.utils.TangemLinks.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
import com.tangem.utils.coroutines.*
import com.tangem.utils.isNullOrZero
import kotlinx.coroutines.NonCancellable
@ -1164,6 +1165,9 @@ internal class SwapModel @Inject constructor(
modelScope.launch { shouldShowStoriesUseCase.neverToShow(StoryContentIds.STORY_FIRST_TIME_SWAP.id) }
swapRouter.openScreen(SwapNavScreen.Main)
},
onOpenLearnMoreAboutApproveClick = {
urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP)
},
)
}

View file

@ -243,6 +243,7 @@ internal class SwapNotificationsFactory(
SwapNotificationUM.Info.PermissionNeeded(
providerName = fromToken.symbol,
fromTokenSymbol = providerName,
onApproveClick = actions.openPermissionBottomSheet,
),
)
}

View file

@ -32,4 +32,5 @@ data class UiActions(
val onSuccess: () -> Unit,
val onLinkClick: (String) -> Unit,
val onReceiveCardWarningClick: () -> Unit,
val onOpenLearnMoreAboutApproveClick: () -> Unit,
)

View file

@ -199,6 +199,7 @@ internal object SwapNotificationUM {
data class PermissionNeeded(
val providerName: String,
val fromTokenSymbol: String,
val onApproveClick: () -> Unit,
) : Info(
title = resourceReference(R.string.express_provider_permission_needed),
subtitle = resourceReference(
@ -206,6 +207,10 @@ internal object SwapNotificationUM {
formatArgs = wrappedList(providerName, fromTokenSymbol),
),
iconResId = R.drawable.ic_locked_24,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = TextReference.Res(R.string.common_approve),
onClick = onApproveClick,
),
)
}
}

View file

@ -347,6 +347,7 @@ internal class StateBuilder(
providerName = swapProvider.name,
onGivePermissionClick = actions.onGivePermissionClick,
onChangeApproveType = actions.onChangeApproveType,
onOpenLearnMoreAboutApproveClick = actions.onOpenLearnMoreAboutApproveClick,
),
fee = feeState,
swapButton = SwapButton(
@ -411,7 +412,8 @@ internal class StateBuilder(
it is SwapNotificationUM.Error || it is NotificationUM.Error ||
it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError ||
it is SwapNotificationUM.Warning.NoAvailableTokensToSwap ||
it is SwapNotificationUM.Warning.NeedReserveToCreateAccount
it is SwapNotificationUM.Warning.NeedReserveToCreateAccount ||
it is SwapNotificationUM.Info.PermissionNeeded
}
}
@ -934,12 +936,14 @@ internal class StateBuilder(
)
}
@Suppress("LongParameterList")
private fun convertPermissionState(
lastPermissionState: GiveTxPermissionState,
permissionDataState: PermissionDataState,
providerName: String,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
onOpenLearnMoreAboutApproveClick: () -> Unit,
): GiveTxPermissionState {
val approveType = if (lastPermissionState is GiveTxPermissionState.ReadyForRequest) {
lastPermissionState.approveType
@ -977,6 +981,7 @@ internal class StateBuilder(
),
dialogText = resourceReference(R.string.swapping_approve_information_text),
footerText = resourceReference(R.string.swap_give_permission_fee_footer),
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
)
}
}

View file

@ -78,7 +78,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
if (state.notifications.isNotEmpty()) SwapNotifications(notifications = state.notifications)
MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet)
SpacerHMax()
if (state.tosState != null && state.providerState !is ProviderState.Empty) {
ProviderTos(
@ -87,6 +87,8 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
.padding(top = TangemTheme.dimens.spacing16),
)
}
MainButton(state = state)
}
if (state.shouldShowMaxAmount && keyboard is Keyboard.Opened) {
@ -359,34 +361,22 @@ private fun SwapNotifications(notifications: List<NotificationUM>) {
}
@Composable
private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> Unit) {
// order is important
when {
state.isInsufficientFunds -> {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.swapping_insufficient_funds),
enabled = false,
onClick = state.swapButton.onClick,
)
}
state.notifications.any { it is SwapNotificationUM.Info.PermissionNeeded } -> {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.give_permission_title),
enabled = true,
onClick = onPermissionWarningClick,
)
}
else -> {
PrimaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.swapping_swap_action),
iconResId = state.swapButton.walletInteractionIcon,
enabled = state.swapButton.enabled,
onClick = state.swapButton.onClick,
)
}
private fun MainButton(state: SwapStateHolder) {
if (state.isInsufficientFunds) {
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.swapping_insufficient_funds),
enabled = false,
onClick = state.swapButton.onClick,
)
} else {
PrimaryButtonIconEnd(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(id = R.string.swapping_swap_action),
iconResId = state.swapButton.walletInteractionIcon,
enabled = state.swapButton.enabled,
onClick = state.swapButton.onClick,
)
}
}
@ -438,6 +428,7 @@ private val state = SwapStateHolder(
SwapNotificationUM.Info.PermissionNeeded(
providerName = "Provider",
fromTokenSymbol = "POL",
onApproveClick = {},
),
SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"),
),