Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-30 20:17:00 +05:00
parent 0d5671c09f
commit fb05f7cf32
16 changed files with 151 additions and 42 deletions

View file

@ -0,0 +1,44 @@
package com.tangem.common.ui.expressStatus
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@Composable
fun ExpressHideButton(isTerminal: Boolean, isAutoDisposable: Boolean, onClick: () -> Unit) {
AnimatedVisibility(
visible = isTerminal && !isAutoDisposable,
label = "Hide button visibility animation",
) {
Text(
text = stringResourceSafe(R.string.express_status_hide_button_text),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.button,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp, end = 12.dp, top = 14.dp)
.clip(RoundedCornerShape(12.dp))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = onClick,
)
.padding(vertical = 10.dp),
)
}
}

View file

@ -30,6 +30,7 @@ data class ExpressTransactionStateInfoUM(
val timestampFormatted: TextReference,
val onGoToProviderClick: (String) -> Unit,
val onClick: () -> Unit,
val onDisposeExpressStatus: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toFiatAmount: TextReference?,

View file

@ -314,6 +314,9 @@
<string name="express_status_bought">Gekauft %s</string>
<string name="express_status_buying">Kauf %s</string>
<string name="express_status_buying_active">Kauf %s…</string>
<string name="express_status_hide_button_text">Diese Transaktion ausblenden</string>
<string name="express_status_hide_dialog_text">Sobald dieser Bildschirm ausgeblendet wurde, kann er nicht mehr aufgerufen werden.</string>
<string name="express_status_hide_dialog_title">Bist Du sicher, dass Du die Transaktionsdetails ausblenden willst?</string>
<string name="express_token_list_empty_search">Keine Token gefunden. Bitte versuche eine andere Anfrage</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaktions-ID kopiert</string>
@ -790,6 +793,8 @@
<string name="staking_migrate">Migrieren</string>
<string name="staking_native">Natives Staking</string>
<string name="staking_no_validators_error_message">Derzeit sind keine aktiven Validatoren für das Staking verfügbar. Bitte versuchen Sie es später erneut.</string>
<string name="staking_notification_additional_ada_deposit_text">Beim Staking im Cardano Netzwerk wird das gesamte Guthaben genutzt. Zusätzlich werden 2 ADA reserviert und nach dem Unstaking zurückgegeben. Deine ADA sind während des Stakings nicht gesperrt.</string>
<string name="staking_notification_additional_ada_deposit_title">Einzelheiten zum ADA-Staking</string>
<string name="staking_notification_claim_rewards_text">Verdiente Belohnungen werden an Deine Wallet gesendet und stehen Dir sofort zur Verfügung</string>
<string name="staking_notification_earn_rewards_text">Stake sicher und verdienen Belohnungen</string>
<string name="staking_notification_earn_rewards_text_daily">Sicher staken und tägliche Belohnungen verdienen.</string>
@ -803,6 +808,8 @@
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
<string name="staking_notification_low_staked_balance_text">Dein verbleibender Stakingbetrag ist zu niedrig, um den Einsatz aufzuheben. Du musst mehr Staken, um den Mindestbetrag zum Aufheben des Einsatzes zu erreichen.</string>
<string name="staking_notification_low_staked_balance_title">Niedriges Stakingguthaben</string>
<string name="staking_notification_minimum_balance_error_text">Für erneutes Staking sind mindestens %1$s %2$s erforderlich. Bitte lade Dein Guthaben auf.</string>
<string name="staking_notification_minimum_balance_error_title">Nicht genug %s</string>
<string name="staking_notification_network_error_text">Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut.</string>
<string name="staking_notification_new_validator_funds_transfer">Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen</string>
<string name="staking_notification_restake_rewards_text">Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn.</string>

View file

@ -648,7 +648,7 @@
<string name="reset_card_with_backup_to_factory_message">工場出荷時設定にリセットすると、選択したカードやリングからウォレットが完全に削除されます。現在のウォレットを復元したり、カードやリングを使用してアクセスコードを復元することはできません。</string>
<string name="reset_card_without_backup_to_factory_message">工場出荷時の状態にリセットすると、選択したカードやリングからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。</string>
<string name="ring_promo_text">Tangem Ringユーザーは、11/15日までChangelly経由でスワップを3回手数料ゼロで行えます</string>
<string name="ring_promo_title">今すぐ手数料0%でスワップしましょう!</string>
<string name="ring_promo_title">今すぐ手数料0% でスワップしましょう!</string>
<string name="save_user_wallet_agreement_access_description">アプリにログインして、カードまたはリングをスキャンせずに残高を確認できます</string>
<string name="save_user_wallet_agreement_access_title">アプリにアクセスする</string>
<string name="save_user_wallet_agreement_allow_biometrics">生体認証の使用を許可する</string>
@ -780,6 +780,8 @@
<string name="staking_migrate">移行</string>
<string name="staking_native">ネイティブステーキング</string>
<string name="staking_no_validators_error_message">現在、ステーキングに使用できるアクティブなバリデータは見つかりません。しばらくしてからもう一度お試しください。</string>
<string name="staking_notification_additional_ada_deposit_text">Cardanoネットワークでステーキングする場合、残高全体が使用されます。追加の2ADAが予約され、ステーキング解除後に返却されます。ステーキング中はADAはロックされません。</string>
<string name="staking_notification_additional_ada_deposit_title">ADAステーキングの詳細</string>
<string name="staking_notification_claim_rewards_text">獲得した報酬はあなたのアドレスに直接送られ、すぐ使用可能です。</string>
<string name="staking_notification_earn_rewards_text">安全にステーキングして報酬を獲得しましょう</string>
<string name="staking_notification_earn_rewards_text_daily">安全にステーキングして、報酬を毎日獲得しましょう</string>
@ -988,7 +990,7 @@
<string name="wallet_network_group_title">%sネットワーク</string>
<string name="wallet_notification_address_copied">アドレスがクリップボードにコピーされました</string>
<string name="wallet_notification_no_internet">インターネット接続がありません</string>
<string name="wallet_promo_banner_button_title">今すぐ10 %オフで購入</string>
<string name="wallet_promo_banner_button_title">今すぐ10 % オフで購入</string>
<string name="wallet_promo_banner_description">1.3万種類以上の暗号資産にアクセス。ワンタップで買付、売却、スワップ、ステーキングが可能です。\nバックアップ用に最大3枚のカードを連携できます。</string>
<string name="wallet_promo_banner_title">Tangemウォレットを見る</string>
<string name="wallet_settings_title">ウォレット設定</string>

View file

@ -142,6 +142,7 @@
<string name="common_generate_addresses">Synchronize addresses</string>
<string name="common_go_to_provider">Go to provider</string>
<string name="common_go_to_token">Go to token</string>
<string name="common_hide">Hide</string>
<string name="common_hour">hour</string>
<string name="common_import">Import</string>
<string name="common_in_progress">In progress</string>
@ -314,6 +315,9 @@
<string name="express_status_bought">Bought %s</string>
<string name="express_status_buying">Buying %s</string>
<string name="express_status_buying_active">Buying %s...</string>
<string name="express_status_hide_button_text">Hide this transaction</string>
<string name="express_status_hide_dialog_text">Once hidden, the transaction status cannot be viewed again. You can simply swipe to dismiss instead.</string>
<string name="express_status_hide_dialog_title">Hide Transaction Status?</string>
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
<string name="express_transaction_id">ID: %s</string>
<string name="express_transaction_id_copied">Transaction ID copied</string>
@ -790,6 +794,8 @@
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_no_validators_error_message">No active validators available for staking at the moment. Please try again later.</string>
<string name="staking_notification_additional_ada_deposit_text">When staking in the Cardano network, the entire balance is used. An additional 2 ADA will be reserved and returned after unstaking. Your ADA is not locked while staking.</string>
<string name="staking_notification_additional_ada_deposit_title">ADA Staking Details</string>
<string name="staking_notification_claim_rewards_text">Earned rewards will be sent to your wallet and available for use immediately</string>
<string name="staking_notification_earn_rewards_text">Stake securely and start earning your rewards</string>
<string name="staking_notification_earn_rewards_text_daily">Stake securely and start earning daily rewards</string>
@ -803,6 +809,8 @@
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_notification_low_staked_balance_text">Your remaining staked balance will be too low to unstake. Youll need to stake more to meet the minimum unstake amount.</string>
<string name="staking_notification_low_staked_balance_title">Low staked balance</string>
<string name="staking_notification_minimum_balance_error_text">A minimum of %1$s %2$s is required for restaking. Please top up your balance.</string>
<string name="staking_notification_minimum_balance_error_title">Not enough %s</string>
<string name="staking_notification_network_error_text">Staking is currently unavailable due to network conditions. Please try again later.</string>
<string name="staking_notification_new_validator_funds_transfer">Staking in the %1$s network with a new validator will automatically transfer all previously staked funds to this validator</string>
<string name="staking_notification_restake_rewards_text">Reinvests your earned rewards in your staked amount, increasing potential earnings.</string>

View file

@ -64,4 +64,16 @@ enum class ExchangeStatus {
@Json(name = "Paused")
Paused,
;
val isTerminal: Boolean
get() = this == Refunded ||
this == Finished ||
this == Cancelled ||
this == TxFailed ||
this == Unknown
val isAutoDisposable: Boolean
get() = this == Finished
}

View file

@ -1,6 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.tokendetails.impl.R
@ -133,5 +134,22 @@ internal data class TokenDetailsDialogConfig(
onClick = onConfirmClick,
)
}
data class ConfirmExpressStatusHideDialogConfig(
val onConfirmClick: () -> Unit,
val onCancelClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = resourceReference(R.string.express_status_hide_dialog_title)
override val message: TextReference = resourceReference(R.string.express_status_hide_dialog_text)
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = resourceReference(R.string.common_hide),
onClick = onConfirmClick,
)
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
text = resourceReference(R.string.common_cancel),
onClick = onCancelClick,
)
}
}
}

View file

@ -15,7 +15,6 @@ internal data class ExchangeUM(
val statuses: ImmutableList<ExchangeStatusState>,
val notification: ExchangeStatusNotifications? = null,
val showProviderLink: Boolean,
val isRefundTerminalStatus: Boolean,
val fromCryptoCurrency: CryptoCurrency,
val toCryptoCurrency: CryptoCurrency,
) : ExpressTransactionStateUM

View file

@ -94,6 +94,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
clickIntents.onGoToProviderClick(it)
},
onClick = { clickIntents.onExpressTransactionClick(value.txId) },
onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus,
),
providerName = value.providerName,
providerImageUrl = value.providerImageUrl,

View file

@ -340,6 +340,22 @@ internal class TokenDetailsStateFactory(
return balanceSelectStateConverter.convert(buttonConfig)
}
fun getStateWithConfirmHideExpressStatus(): TokenDetailsState {
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig(
onConfirmClick = {
clickIntents.onDisposeExpressStatus()
clickIntents.onDismissDialog()
},
onCancelClick = clickIntents::onDismissDialog,
),
),
)
}
private fun TokenDetailsAppBarMenuConfig.updateMenu(
cardTypesResolver: CardTypesResolver,
hasDerivations: Boolean,

View file

@ -90,7 +90,6 @@ internal class TokenDetailsSwapTransactionsStateConverter(
notification = notifications,
activeStatus = transaction.status?.status,
showProviderLink = showProviderLink,
isRefundTerminalStatus = true,
fromCryptoCurrency = fromCryptoCurrency,
toCryptoCurrency = toCryptoCurrency,
info = createStateInfo(
@ -107,12 +106,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
return result.toPersistentList()
}
fun updateTxStatus(
tx: ExchangeUM,
statusModel: ExchangeStatusModel?,
refundToken: CryptoCurrency?,
isRefundTerminalStatus: Boolean,
): ExchangeUM {
fun updateTxStatus(tx: ExchangeUM, statusModel: ExchangeStatusModel?, refundToken: CryptoCurrency?): ExchangeUM {
if (statusModel == null || tx.activeStatus == statusModel.status) {
Timber.e("UpdateTxStatus isn't required. Current status isn't changed")
return tx
@ -125,7 +119,6 @@ internal class TokenDetailsSwapTransactionsStateConverter(
notification = notifications,
statuses = getStatuses(statusModel.status, hasFailed),
showProviderLink = showProviderLink,
isRefundTerminalStatus = isRefundTerminalStatus,
info = tx.info.copy(txExternalUrl = statusModel.txExternalUrl),
)
}
@ -164,7 +157,8 @@ internal class TokenDetailsSwapTransactionsStateConverter(
},
iconState = getIconState(transaction.status?.status),
status = getStatusState(),
notification = null, // fixme [REDACTED_JIRA]
notification = null, // fixme [REDACTED_JIRA],
onDisposeExpressStatus = clickIntents::onConfirmDisposeExpressStatus,
)
}

View file

@ -79,13 +79,13 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
}
suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) {
suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) {
val state = currentStateProvider()
val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return
val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return
val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal
if (shouldTerminate) {
val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose
if (shouldDispose) {
swapTransactionRepository.removeTransaction(
userWalletId = userWalletId,
fromCryptoCurrency = selectedTx.fromCryptoCurrency,
@ -96,20 +96,16 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
}
suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM {
return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) {
return if (swapTx.activeStatus?.isTerminal == true) {
swapTx
} else {
val statusModel = getExchangeStatus(swapTx.info.txId, swapTx.provider)
val isRefundTerminalStatus = statusModel?.refundNetwork == null &&
statusModel?.refundContractAddress == null &&
swapTx.provider.type != ExchangeProviderType.DEX_BRIDGE
val addedRefundToken = addRefundCurrencyIfNeeded(statusModel, swapTx.provider.type)
swapTransactionsStateConverter.updateTxStatus(
tx = swapTx,
statusModel = statusModel,
refundToken = addedRefundToken,
isRefundTerminalStatus = isRefundTerminalStatus,
)
}
}
@ -174,15 +170,6 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
)
}
private fun ExchangeStatus?.isTerminal(isRefundTerminal: Boolean): Boolean {
val needTerminalRefund = this == ExchangeStatus.Refunded && isRefundTerminal
return needTerminalRefund ||
this == ExchangeStatus.Finished ||
this == ExchangeStatus.Cancelled ||
this == ExchangeStatus.TxFailed ||
this == ExchangeStatus.Unknown
}
private fun toAnalyticStatus(status: ExchangeStatus?): ExpressAnalyticsStatus? {
return when (status) {
ExchangeStatus.New,

View file

@ -157,11 +157,13 @@ internal class ExpressStatusFactory @AssistedInject constructor(
suspend fun removeTransactionOnBottomSheetClosed(
expressState: ExpressTransactionStateUM,
isForceTerminal: Boolean = false,
isForceDispose: Boolean = false,
) {
when (expressState) {
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed()
is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceDispose)
is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed(
isForceDispose,
)
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import com.tangem.common.ui.expressStatus.ExpressEstimate
import com.tangem.common.ui.expressStatus.ExpressHideButton
import com.tangem.common.ui.expressStatus.ExpressProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
@ -70,6 +71,11 @@ internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) {
if (state.notification != null) {
Notification(state = state.notification, activeStatus = state.activeStatus)
}
ExpressHideButton(
isTerminal = state.activeStatus?.isTerminal == true,
isAutoDisposable = state.activeStatus?.isAutoDisposable == true,
onClick = state.info.onDisposeExpressStatus,
)
SpacerH24()
}
}

View file

@ -72,4 +72,8 @@ interface TokenDetailsClickIntents {
fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency)
fun onOpenUrlClick(url: String)
fun onConfirmDisposeExpressStatus()
fun onDisposeExpressStatus()
}

View file

@ -784,16 +784,7 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onGoToRefundedTokenClick(cryptoCurrency: CryptoCurrency) {
val bottomSheetState = internalUiState.value.bottomSheetConfig?.content
if (bottomSheetState is ExpressStatusBottomSheetConfig) {
viewModelScope.launch {
expressStatusFactory.removeTransactionOnBottomSheetClosed(
expressState = bottomSheetState.value,
isForceTerminal = true,
)
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
onDisposeExpressStatus()
router.openTokenDetails(userWalletId, cryptoCurrency)
}
@ -950,6 +941,23 @@ internal class TokenDetailsViewModel @Inject constructor(
internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config)
}
override fun onConfirmDisposeExpressStatus() {
internalUiState.value = stateFactory.getStateWithConfirmHideExpressStatus()
}
override fun onDisposeExpressStatus() {
val bottomSheetState = internalUiState.value.bottomSheetConfig?.content
if (bottomSheetState is ExpressStatusBottomSheetConfig) {
viewModelScope.launch {
expressStatusFactory.removeTransactionOnBottomSheetClosed(
expressState = bottomSheetState.value,
isForceDispose = true,
)
}
}
internalUiState.value = stateFactory.getStateWithClosedBottomSheet()
}
private fun handleUnavailabilityReason(
unavailabilityReason: ScenarioUnavailabilityReason,
tokenSymbol: String,