Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-12 13:43:44 +05:00
parent f6b98ab9fa
commit db16ba6cef
20 changed files with 618 additions and 96 deletions

View file

@ -98,7 +98,7 @@ class TangemLoggingInitializer(
BlockchainSdkConfig.serializer(), BlockchainSdkConfig.serializer(),
environmentConfig.blockchainSdkConfig, environmentConfig.blockchainSdkConfig,
) )
// Drop URL-shaped values (e.g. public endpoint URLs from BlockchainSdkConfig like // Drop URL-shaped drawable (e.g. public endpoint URLs from BlockchainSdkConfig like
// kaspaSecondaryApiUrl); they are not secrets and would obscure unrelated requests in logs. // kaspaSecondaryApiUrl); they are not secrets and would obscure unrelated requests in logs.
val values = JsonStringValuesExtractor.extract(json) val values = JsonStringValuesExtractor.extract(json)
.filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) } .filter { it.isNotBlank() && !it.startsWith("http", ignoreCase = true) }

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<paths> <paths>
<files-path name="files" path="/"/> <files-path name="files" path="/"/>
<cache-path name="shared_images" path="." />
</paths> </paths>

View file

@ -2,9 +2,9 @@ package com.tangem.common.ui.expressStatus
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
data class ExpressStatusBottomSheetConfig( data class ExpressStatusBottomSheetConfig(
@ -18,7 +18,7 @@ fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
containerColor = TangemTheme.colors.background.tertiary, containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig -> ) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) { when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state) is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state, false)
} }
} }
} }

View file

@ -3,13 +3,7 @@ package com.tangem.common.ui.expressStatus
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -22,11 +16,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM import com.tangem.common.ui.expressStatus.state.*
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon
@ -221,13 +211,17 @@ private val PreviewExpressTransactionState: ExpressTransactionStateUM = object :
onDisposeExpressStatus = {}, onDisposeExpressStatus = {},
iconState = ExpressTransactionStateIconUM.None, iconState = ExpressTransactionStateIconUM.None,
toAmount = stringReference("0,11441958 BTC"), toAmount = stringReference("0,11441958 BTC"),
toAmountValue = "0.11441958".toBigDecimal(),
toFiatAmount = null, toFiatAmount = null,
toAmountSymbol = "BTC", toAmountSymbol = "BTC",
toCurrencyIcon = CurrencyIconState.Loading, toCurrencyIcon = CurrencyIconState.Loading,
toAddress = "0x",
fromAmount = stringReference("100 SOL"), fromAmount = stringReference("100 SOL"),
fromAmountValue = "100".toBigDecimal(),
fromFiatAmount = null, fromFiatAmount = null,
fromAmountSymbol = "SOL", fromAmountSymbol = "SOL",
fromCurrencyIcon = CurrencyIconState.Loading, fromCurrencyIcon = CurrencyIconState.Loading,
fromAddress = "0x",
) )
} }
// endregion // endregion

View file

@ -9,18 +9,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@Composable @Composable
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) { fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM, isExpressShareButtonEnabled: Boolean) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16) .padding(horizontal = TangemTheme.dimens.spacing16)
@ -70,6 +68,10 @@ fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
isAutoDisposable = state.activeStatus.isAutoDisposable, isAutoDisposable = state.activeStatus.isAutoDisposable,
onClick = state.info.onDisposeExpressStatus, onClick = state.info.onDisposeExpressStatus,
) )
if (isExpressShareButtonEnabled) {
SpacerH(80.dp)
} else {
SpacerH24() SpacerH24()
} }
}
} }

View file

@ -1,10 +1,13 @@
package com.tangem.common.ui.expressStatus.state package com.tangem.common.ui.expressStatus.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.OnrampStatus
import java.math.BigDecimal
@Immutable
interface ExpressTransactionStateUM { interface ExpressTransactionStateUM {
val info: ExpressTransactionStateInfoUM val info: ExpressTransactionStateInfoUM
@ -35,13 +38,17 @@ data class ExpressTransactionStateInfoUM(
val onDisposeExpressStatus: () -> Unit, val onDisposeExpressStatus: () -> Unit,
val iconState: ExpressTransactionStateIconUM, val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference, val toAmount: TextReference,
val toAmountValue: BigDecimal,
val toFiatAmount: TextReference?, val toFiatAmount: TextReference?,
val toAmountSymbol: String, val toAmountSymbol: String,
val toCurrencyIcon: CurrencyIconState, val toCurrencyIcon: CurrencyIconState,
val toAddress: String,
val fromAmount: TextReference, val fromAmount: TextReference,
val fromAmountValue: BigDecimal,
val fromFiatAmount: TextReference?, val fromFiatAmount: TextReference?,
val fromAmountSymbol: String, val fromAmountSymbol: String,
val fromCurrencyIcon: CurrencyIconState, val fromCurrencyIcon: CurrencyIconState,
val fromAddress: String?,
) { ) {
val subtitle: TextReference val subtitle: TextReference
get() = buildExpressStatusSubtitle(activeStatus = activeStatus, date = timestampAgoFormatted) get() = buildExpressStatusSubtitle(activeStatus = activeStatus, date = timestampAgoFormatted)

View file

@ -138,5 +138,9 @@
{ {
"name": "TWI_83_ADDRESS_BOOK_ENABLED", "name": "TWI_83_ADDRESS_BOOK_ENABLED",
"version": "undefined" "version": "undefined"
},
{
"name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED",
"version": "undefined"
} }
] ]

View file

@ -8,4 +8,5 @@ interface SwapFeatureToggles {
val isSwapProviderFilterEnabled: Boolean val isSwapProviderFilterEnabled: Boolean
val isSwapRateExperienceEnabled: Boolean val isSwapRateExperienceEnabled: Boolean
val isSwapPredefinedButtonsEnabled: Boolean val isSwapPredefinedButtonsEnabled: Boolean
val isExpressShareButtonEnabled: Boolean
} }

View file

@ -6,34 +6,45 @@ import com.tangem.features.swap.SwapFeatureToggles
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSwapFeatureToggles @Inject constructor( internal class DefaultSwapFeatureToggles @Inject constructor(
featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
) : SwapFeatureToggles { ) : SwapFeatureToggles {
override val isYieldSwapEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isYieldSwapEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED, toggle = FeatureToggles.TWI_1326_YIELD_MODE_SWAP_ENABLED,
) )
override val isSwapSwitchToTransferEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isSwapSwitchToTransferEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED, toggle = FeatureToggles.AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED,
) )
override val isSwapIntegratedApproveEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isSwapIntegratedApproveEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE, toggle = FeatureToggles.AND_15120_SWAP_INTEGRATED_APPROVE,
) )
override val isSwapAbEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isSwapAbEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.SWAP_AB_ENABLED, toggle = FeatureToggles.SWAP_AB_ENABLED,
) )
override val isSwapProviderFilterEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isSwapProviderFilterEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15009_SWAP_PROVIDER_FILTER_ENABLED, toggle = FeatureToggles.AND_15009_SWAP_PROVIDER_FILTER_ENABLED,
) )
override val isSwapRateExperienceEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isSwapRateExperienceEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15103_SWAP_RATE_EXPERIENCE_ENABLED, toggle = FeatureToggles.AND_15103_SWAP_RATE_EXPERIENCE_ENABLED,
) )
override val isSwapPredefinedButtonsEnabled: Boolean = featureTogglesManager.isFeatureEnabled( override val isSwapPredefinedButtonsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED, toggle = FeatureToggles.AND_15122_SWAP_PREDEFINED_BUTTONS_ENABLED,
) )
override val isExpressShareButtonEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(
toggle = FeatureToggles.AND_15489_EXPRESS_SHARE_BUTTON_ENABLED,
)
} }

View file

@ -3,14 +3,7 @@ package com.tangem.features.tangempay.components.express
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.tangem.common.ui.expressStatus.expressTransactionsItemsLegacy import com.tangem.common.ui.expressStatus.expressTransactionsItemsLegacy
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM import com.tangem.common.ui.expressStatus.state.*
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState
import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.OnrampStatus
@ -122,13 +115,17 @@ internal class PreviewEmptyExpressTransactionsComponent : ExpressTransactionsCom
onDisposeExpressStatus = {}, onDisposeExpressStatus = {},
iconState = iconState, iconState = iconState,
toAmount = TextReference.Str(toAmount), toAmount = TextReference.Str(toAmount),
toAmountValue = toAmount.toBigDecimal(),
toFiatAmount = null, toFiatAmount = null,
toAmountSymbol = toSymbol, toAmountSymbol = toSymbol,
toCurrencyIcon = CurrencyIconState.Empty(), toCurrencyIcon = CurrencyIconState.Empty(),
toAddress = "",
fromAmount = TextReference.Str(fromAmount), fromAmount = TextReference.Str(fromAmount),
fromAmountValue = fromAmount.toBigDecimal(),
fromFiatAmount = null, fromFiatAmount = null,
fromAmountSymbol = fromSymbol, fromAmountSymbol = fromSymbol,
fromCurrencyIcon = CurrencyIconState.Empty(), fromCurrencyIcon = CurrencyIconState.Empty(),
fromAddress = "",
), ),
providerName = "Preview Provider", providerName = "Preview Provider",
providerImageUrl = "", providerImageUrl = "",

View file

@ -1,5 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.common.ui.expressStatus.state.* import com.tangem.common.ui.expressStatus.state.*
import com.tangem.common.ui.expressStatus.toActiveStatusText import com.tangem.common.ui.expressStatus.toActiveStatusText
import com.tangem.common.ui.expressStatus.toIconState import com.tangem.common.ui.expressStatus.toIconState
@ -7,7 +8,6 @@ import com.tangem.common.ui.notifications.ExpressNotificationsUM
import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.wrappedList
@ -41,6 +41,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
override fun convert(value: OnrampTransaction): ExpressTransactionStateUM.OnrampUM { override fun convert(value: OnrampTransaction): ExpressTransactionStateUM.OnrampUM {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val statusValue = cryptoCurrencyStatus?.value
val appCurrency = appCurrencyProvider() val appCurrency = appCurrencyProvider()
return ExpressTransactionStateUM.OnrampUM( return ExpressTransactionStateUM.OnrampUM(
info = ExpressTransactionStateInfoUM( info = ExpressTransactionStateInfoUM(
@ -63,8 +64,9 @@ internal class TokenDetailsOnrampTransactionStateConverter(
toAmount = stringReference( toAmount = stringReference(
value.toAmount.format { crypto(cryptoCurrency) }, value.toAmount.format { crypto(cryptoCurrency) },
), ),
toAmountValue = value.toAmount,
toFiatAmount = stringReference( toFiatAmount = stringReference(
cryptoCurrencyStatus?.value?.fiatRate?.multiply(value.toAmount).format { statusValue?.fiatRate?.multiply(value.toAmount).format {
fiat( fiat(
fiatCurrencyCode = appCurrency.code, fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol, fiatCurrencySymbol = appCurrency.symbol,
@ -73,6 +75,7 @@ internal class TokenDetailsOnrampTransactionStateConverter(
), ),
toAmountSymbol = cryptoCurrency.symbol, toAmountSymbol = cryptoCurrency.symbol,
toCurrencyIcon = iconStateConverter.convert(cryptoCurrency), toCurrencyIcon = iconStateConverter.convert(cryptoCurrency),
toAddress = statusValue?.networkAddress?.defaultAddress?.value.orEmpty(),
fromAmount = stringReference( fromAmount = stringReference(
value.fromAmount.format { value.fromAmount.format {
fiat( fiat(
@ -81,12 +84,14 @@ internal class TokenDetailsOnrampTransactionStateConverter(
) )
}, },
), ),
fromAmountValue = value.fromAmount,
fromFiatAmount = null, fromFiatAmount = null,
fromAmountSymbol = value.fromCurrency.code, fromAmountSymbol = value.fromCurrency.code,
fromCurrencyIcon = CurrencyIconState.FiatIcon( fromCurrencyIcon = CurrencyIconState.FiatIcon(
url = value.fromCurrency.image, url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24, fallbackResId = R.drawable.ic_currency_24,
), ),
fromAddress = null,
iconState = value.status.toIconState(), iconState = value.status.toIconState(),
onGoToProviderClick = { url -> onGoToProviderClick = { url ->
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider())

View file

@ -1,11 +1,11 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
@ -17,9 +17,9 @@ import com.tangem.core.ui.utils.mapFormattedDate
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.quote.mapData
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
@ -34,11 +34,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.E
import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal import java.math.BigDecimal
// Fixme [REDACTED_JIRA] // Fixme [REDACTED_JIRA]
@ -59,7 +59,7 @@ internal class TokenDetailsSwapTransactionsStateConverter(
fun convert( fun convert(
savedTransactions: List<SavedSwapTransactionListModel>, savedTransactions: List<SavedSwapTransactionListModel>,
quoteStatuses: Set<QuoteStatus>, accountStatuses: Map<Account, CryptoCurrencyStatus>,
): PersistentList<ExchangeUM> { ): PersistentList<ExchangeUM> {
val result = mutableListOf<ExchangeUM>() val result = mutableListOf<ExchangeUM>()
@ -69,23 +69,25 @@ internal class TokenDetailsSwapTransactionsStateConverter(
val fromCryptoCurrency = swapTransaction.fromCryptoCurrency val fromCryptoCurrency = swapTransaction.fromCryptoCurrency
val toCryptoCurrencyRawId = swapTransaction.toCryptoCurrency.id.rawCurrencyId val toCryptoCurrencyRawId = swapTransaction.toCryptoCurrency.id.rawCurrencyId
val fromCryptoCurrencyRawId = swapTransaction.fromCryptoCurrency.id.rawCurrencyId val fromCryptoCurrencyRawId = swapTransaction.fromCryptoCurrency.id.rawCurrencyId
var fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null
var toCryptoCurrencyStatus: CryptoCurrencyStatus? = null
swapTransaction.transactions.forEach { transaction -> swapTransaction.transactions.forEach { transaction ->
val toAmount = transaction.toCryptoAmount val toAmount = transaction.toCryptoAmount
val fromAmount = transaction.fromCryptoAmount val fromAmount = transaction.fromCryptoAmount
var toFiatAmount: BigDecimal? = null var toFiatAmount: BigDecimal? = null
var fromFiatAmount: BigDecimal? = null var fromFiatAmount: BigDecimal? = null
quoteStatuses.forEach { quote -> accountStatuses.forEach { (account, cryptoCurrencyStatus) ->
quote.mapData { if (cryptoCurrencyStatus.currency.id.rawCurrencyId == fromCryptoCurrencyRawId &&
if (quote.rawCurrencyId == toCryptoCurrencyRawId) { account.userWalletId.stringValue == swapTransaction.fromUserWalletId
toFiatAmount = fiatRate.multiply(toAmount) ) {
} fromFiatAmount = cryptoCurrencyStatus.value.fiatRate?.multiply(fromAmount)
} fromCryptoCurrencyStatus = cryptoCurrencyStatus
quote.mapData {
if (quote.rawCurrencyId == fromCryptoCurrencyRawId) {
fromFiatAmount = fiatRate.multiply(fromAmount)
} }
if (cryptoCurrencyStatus.currency.id.rawCurrencyId == toCryptoCurrencyRawId &&
account.userWalletId.stringValue == swapTransaction.toUserWalletId
) {
toFiatAmount = cryptoCurrencyStatus.value.fiatRate?.multiply(toAmount)
toCryptoCurrencyStatus = cryptoCurrencyStatus
} }
} }
val statusModel = transaction.status val statusModel = transaction.status
@ -111,6 +113,8 @@ internal class TokenDetailsSwapTransactionsStateConverter(
transaction = transaction, transaction = transaction,
toCryptoCurrency = toCryptoCurrency, toCryptoCurrency = toCryptoCurrency,
fromCryptoCurrency = fromCryptoCurrency, fromCryptoCurrency = fromCryptoCurrency,
fromCryptoCurrencyStatus = fromCryptoCurrencyStatus,
toCryptoCurrencyStatus = toCryptoCurrencyStatus,
toFiatAmount = toFiatAmount, toFiatAmount = toFiatAmount,
fromFiatAmount = fromFiatAmount, fromFiatAmount = fromFiatAmount,
), ),
@ -149,14 +153,19 @@ internal class TokenDetailsSwapTransactionsStateConverter(
) )
} }
@Suppress("LongParameterList")
private fun createStateInfo( private fun createStateInfo(
transaction: SavedSwapTransactionModel, transaction: SavedSwapTransactionModel,
toCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency,
fromCryptoCurrency: CryptoCurrency, fromCryptoCurrency: CryptoCurrency,
fromCryptoCurrencyStatus: CryptoCurrencyStatus?,
toCryptoCurrencyStatus: CryptoCurrencyStatus?,
toFiatAmount: BigDecimal?, toFiatAmount: BigDecimal?,
fromFiatAmount: BigDecimal?, fromFiatAmount: BigDecimal?,
): ExpressTransactionStateInfoUM { ): ExpressTransactionStateInfoUM {
val timestamp = transaction.timestamp val timestamp = transaction.timestamp
val fromStatusValue = fromCryptoCurrencyStatus?.value
val toStatusValue = toCryptoCurrencyStatus?.value
return ExpressTransactionStateInfoUM( return ExpressTransactionStateInfoUM(
title = resourceReference(R.string.express_exchange_by, wrappedList(transaction.provider.name)), title = resourceReference(R.string.express_exchange_by, wrappedList(transaction.provider.name)),
txId = transaction.txId, txId = transaction.txId,
@ -169,13 +178,17 @@ internal class TokenDetailsSwapTransactionsStateConverter(
timestampAgoFormatted = mapFormattedDate(timestamp), timestampAgoFormatted = mapFormattedDate(timestamp),
activeStatus = getActiveStatusText(transaction.status?.status), activeStatus = getActiveStatusText(transaction.status?.status),
toAmount = getCryptoAmount(transaction.toCryptoAmount, toCryptoCurrency), toAmount = getCryptoAmount(transaction.toCryptoAmount, toCryptoCurrency),
toAmountValue = transaction.toCryptoAmount,
toFiatAmount = getFiatAmount(toFiatAmount), toFiatAmount = getFiatAmount(toFiatAmount),
toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency), toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency),
toAmountSymbol = toCryptoCurrency.symbol, toAmountSymbol = toCryptoCurrency.symbol,
toAddress = toStatusValue?.networkAddress?.defaultAddress?.value.orEmpty(),
fromAmount = getCryptoAmount(transaction.fromCryptoAmount, fromCryptoCurrency), fromAmount = getCryptoAmount(transaction.fromCryptoAmount, fromCryptoCurrency),
fromAmountValue = transaction.fromCryptoAmount,
fromFiatAmount = getFiatAmount(fromFiatAmount), fromFiatAmount = getFiatAmount(fromFiatAmount),
fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency), fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency),
fromAmountSymbol = fromCryptoCurrency.symbol, fromAmountSymbol = fromCryptoCurrency.symbol,
fromAddress = fromStatusValue?.networkAddress?.defaultAddress?.value.orEmpty(),
onClick = { clickIntents.onExpressTransactionClick(transaction.txId) }, onClick = { clickIntents.onExpressTransactionClick(transaction.txId) },
onGoToProviderClick = { url -> onGoToProviderClick = { url ->
analyticsEventsHandler.send( analyticsEventsHandler.send(

View file

@ -8,12 +8,13 @@ import com.tangem.datasource.local.swap.SwapTransactionStatusStore
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.SwapTransactionRepository
@ -37,8 +38,8 @@ import kotlinx.coroutines.flow.map
internal class ExchangeStatusFactory @AssistedInject constructor( internal class ExchangeStatusFactory @AssistedInject constructor(
private val swapTransactionRepository: SwapTransactionRepository, private val swapTransactionRepository: SwapTransactionRepository,
private val swapRepository: SwapRepository, private val swapRepository: SwapRepository,
private val quotesRepository: QuotesRepository,
private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase,
private val getPaymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase,
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
private val swapTransactionStatusStore: SwapTransactionStatusStore, private val swapTransactionStatusStore: SwapTransactionStatusStore,
private val analyticsEventsHandler: AnalyticsEventHandler, private val analyticsEventsHandler: AnalyticsEventHandler,
@ -62,19 +63,23 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
operator fun invoke(): Flow<PersistentList<ExchangeUM>> { operator fun invoke(): Flow<PersistentList<ExchangeUM>> {
return swapTransactionRepository.getTransactions( return swapTransactionRepository.getTransactions(
userWallet = userWallet, userWallet = userWallet,
cryptoCurrencyId = cryptoCurrency.id, cryptoCurrencyId = cryptoCurrency.id,
).conflate() ).conflate()
.map { savedTransactions -> .map { savedTransactions ->
val quotes = savedTransactions val accountStatuses = savedTransactions
?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } ?.flatMap { swapTransaction ->
?.toSet() setOf(
?.getQuotesOrEmpty() swapTransaction.fromAccount to swapTransaction.fromCryptoCurrency,
?: emptySet() swapTransaction.toAccount to swapTransaction.toCryptoCurrency,
)
}
?.toMap()
?.getStatuses()
.orEmpty()
getExchangeStatusState( getExchangeStatusState(
savedTransactions = savedTransactions, savedTransactions = savedTransactions,
quoteStatuses = quotes, accountStatuses = accountStatuses,
) )
} }
} }
@ -194,7 +199,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
private fun getExchangeStatusState( private fun getExchangeStatusState(
savedTransactions: List<SavedSwapTransactionListModel>?, savedTransactions: List<SavedSwapTransactionListModel>?,
quoteStatuses: Set<QuoteStatus>, accountStatuses: Map<Account, CryptoCurrencyStatus>,
): PersistentList<ExchangeUM> { ): PersistentList<ExchangeUM> {
if (savedTransactions == null) { if (savedTransactions == null) {
return persistentListOf() return persistentListOf()
@ -202,7 +207,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
return swapTransactionsStateConverter.convert( return swapTransactionsStateConverter.convert(
savedTransactions = savedTransactions, savedTransactions = savedTransactions,
quoteStatuses = quoteStatuses, accountStatuses = accountStatuses,
) )
} }
@ -226,12 +231,24 @@ internal class ExchangeStatusFactory @AssistedInject constructor(
} }
} }
private suspend fun Set<CryptoCurrency.ID>.getQuotesOrEmpty(): Set<QuoteStatus> { private suspend fun Map<Account?, CryptoCurrency>.getStatuses(): Map<Account, CryptoCurrencyStatus> {
val rawIds = mapNotNull { it.rawCurrencyId }.toSet() return mapNotNull { (account, cryptoCurrency) ->
when (account) {
is Account.CryptoPortfolio -> {
val (cryptoPortfolioAccount, currencyStatus) = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = account.userWalletId,
currency = cryptoCurrency,
).getOrNull() ?: return@mapNotNull null
return runCatching { quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds) } cryptoPortfolioAccount to currencyStatus
.getOrNull() }
.orEmpty() is Account.Payment -> getPaymentAccountCryptoCurrencyStatusUseCase.invokeSync(
userWalletId = account.userWalletId,
cryptoCurrency = cryptoCurrency,
).getOrNull()
else -> null
}
}.toMap()
} }
@AssistedFactory @AssistedFactory

View file

@ -19,6 +19,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTr
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.Provider import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted import dagger.assisted.Assisted
@ -45,6 +46,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
private val analyticsEventsHandler: AnalyticsEventHandler, private val analyticsEventsHandler: AnalyticsEventHandler,
onrampStatusFactory: OnrampStatusFactory.Factory, onrampStatusFactory: OnrampStatusFactory.Factory,
exchangeStatusFactory: ExchangeStatusFactory.Factory, exchangeStatusFactory: ExchangeStatusFactory.Factory,
private val swapFeatureToggles: SwapFeatureToggles,
) { ) {
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -212,6 +214,7 @@ internal class ExpressStatusFactory @AssistedInject constructor(
is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet( is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(
config = this, config = this,
extraContent = extraContent, extraContent = extraContent,
isExpressShareButtonEnabled = swapFeatureToggles.isExpressShareButtonEnabled,
) )
} }
} }

View file

@ -1,14 +1,18 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.OnrampStatusBottomSheetContent import com.tangem.common.ui.expressStatus.OnrampStatusBottomSheetContent
import com.tangem.common.ui.expressStatus.state.* import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@ -17,15 +21,44 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e
@Composable @Composable
internal fun ExpressStatusBottomSheet( internal fun ExpressStatusBottomSheet(
config: TangemBottomSheetConfig, config: TangemBottomSheetConfig,
isExpressShareButtonEnabled: Boolean,
extraContent: (@Composable () -> Unit)? = null, extraContent: (@Composable () -> Unit)? = null,
) { ) {
TangemBottomSheet( TangemBottomSheet(
config = config, config = config,
containerColor = TangemTheme.colors.background.tertiary, containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig -> ) { content: ExpressStatusBottomSheetConfig ->
if (isExpressShareButtonEnabled) {
Box {
when (val state = content.value) { when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state) is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(
is ExchangeUM -> ExchangeStatusBottomSheetContent(state = state, extraContent = extraContent) state = state,
isExpressShareButtonEnabled = true,
)
is ExchangeUM -> ExchangeStatusBottomSheetContent(
state = state,
extraContent = extraContent,
isExpressShareButtonEnabled = true,
)
}
BottomFade(
backgroundColor = TangemTheme.colors.background.tertiary,
modifier = Modifier.align(Alignment.BottomCenter),
)
ExpressShareContent(state = content.value)
}
} else {
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(
state = state,
isExpressShareButtonEnabled = false,
)
is ExchangeUM -> ExchangeStatusBottomSheetContent(
state = state,
extraContent = extraContent,
isExpressShareButtonEnabled = false,
)
}
} }
} }
} }
@ -43,6 +76,7 @@ private fun PreviewExpressStatusBottomSheet(
onDismissRequest = {}, onDismissRequest = {},
content = param, content = param,
), ),
isExpressShareButtonEnabled = false,
) )
} }
} }

View file

@ -24,7 +24,7 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider<ExpressSt
get() = sequenceOf( get() = sequenceOf(
ExpressStatusBottomSheetConfig( ExpressStatusBottomSheetConfig(
ExchangeUM( ExchangeUM(
fromUserWalletId = UserWalletId("AnyUserWalletID"), fromUserWalletId = UserWalletId("0123456789ABCDEF"),
info = ExpressTransactionStateInfoUM( info = ExpressTransactionStateInfoUM(
title = TextReference.Str("Transaction Status"), title = TextReference.Str("Transaction Status"),
status = ExpressStatusUM( status = ExpressStatusUM(
@ -49,13 +49,17 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider<ExpressSt
onDisposeExpressStatus = {}, onDisposeExpressStatus = {},
iconState = ExpressTransactionStateIconUM.None, iconState = ExpressTransactionStateIconUM.None,
toAmount = TextReference.Str("0.1 BTC"), toAmount = TextReference.Str("0.1 BTC"),
toAmountValue = "0.1".toBigDecimal(),
toFiatAmount = TextReference.Str("$5000"), toFiatAmount = TextReference.Str("$5000"),
toAmountSymbol = "BTC", toAmountSymbol = "BTC",
toCurrencyIcon = CurrencyIconState.Empty(), toCurrencyIcon = CurrencyIconState.Empty(),
toAddress = "0x",
fromAmount = TextReference.Str("5000 USDT"), fromAmount = TextReference.Str("5000 USDT"),
fromAmountValue = "5000".toBigDecimal(),
fromFiatAmount = TextReference.Str("$5000"), fromFiatAmount = TextReference.Str("$5000"),
fromAmountSymbol = "USDT", fromAmountSymbol = "USDT",
fromCurrencyIcon = CurrencyIconState.Empty(), fromCurrencyIcon = CurrencyIconState.Empty(),
fromAddress = "0x",
), ),
provider = SwapProvider( provider = SwapProvider(
providerId = "1", providerId = "1",
@ -67,14 +71,20 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider<ExpressSt
privacyPolicy = null, privacyPolicy = null,
slippage = BigDecimal.ZERO, slippage = BigDecimal.ZERO,
), ),
activeStatus = null, activeStatus = ExchangeStatus.Failed,
statuses = persistentListOf( statuses = persistentListOf(
ExchangeStatusState( ExchangeStatusState(
status = ExchangeStatus.Exchanging, status = ExchangeStatus.Exchanging,
text = stringReference("Exchanging"), text = stringReference("Exchanging"),
isActive = true, isActive = false,
isDone = false, isDone = false,
), ),
ExchangeStatusState(
status = ExchangeStatus.Failed,
text = stringReference("Failed"),
isActive = true,
isDone = true,
),
), ),
notification = ExchangeStatusNotification.LongTimeExchange( notification = ExchangeStatusNotification.LongTimeExchange(
onGoToProviderClick = {}, onGoToProviderClick = {},

View file

@ -0,0 +1,409 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.TangemThemeRedesign
import com.tangem.core.ui.utils.toPx
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.extensions.stripZeroPlainString
import kotlinx.coroutines.launch
import java.io.File
import java.math.BigDecimal
@Composable
internal fun BoxScope.ExpressShareContent(state: ExpressTransactionStateUM) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var graphicsLayer = rememberGraphicsLayer()
val shareText = makeExpressShareContent(state) ?: return
TangemThemeRedesign {
SecondaryButtonIconStart(
text = stringResourceSafe(R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
coroutineScope.launch {
if (graphicsLayer.size.width > 0 && graphicsLayer.size.height > 0) {
val uri = graphicsLayer.saveAsShareableFile(context)
val shareIntent = if (uri != null) {
Intent().apply {
action = Intent.ACTION_SEND
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // Temporary external read grant
putExtra(Intent.EXTRA_TEXT, shareText)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
type = "image/png"
}
} else {
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, shareText)
type = "text/plain"
}
}
context.startActivity(Intent.createChooser(shareIntent, null))
}
}
},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.align(Alignment.BottomCenter),
)
ExpressShareImageContent(
state = state,
onGraphicsLayer = { layer -> graphicsLayer = layer },
modifier = Modifier.size(0.dp), // size 0 so that no space is used in the UI
)
}
}
@Composable
private fun ExpressShareImageContent(
state: ExpressTransactionStateUM,
onGraphicsLayer: (GraphicsLayer) -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier.drawForShare(onGraphicsLayer)) {
Box(
// override the parent size with desired size of the recording
modifier = Modifier
.wrapContentHeight(unbounded = true, align = Alignment.Top)
.wrapContentWidth(unbounded = true, align = Alignment.Start)
.requiredSize(525.dp, 580.dp),
) {
Column(
modifier = Modifier
.matchParentSize()
.background(TangemColorPalette.Black),
) {
Column(
modifier = Modifier.padding(
start = 40.dp,
end = 40.dp,
top = 40.dp,
bottom = 20.dp,
),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.img_tangem_logo_90_24),
contentDescription = null,
tint = TangemColorPalette.White,
)
SpacerH(36.dp)
ExpressShareImageAmount(
prefix = stringResourceSafe(R.string.common_send),
amountValue = state.info.fromAmountValue,
currencyIconState = state.info.fromCurrencyIcon,
currencySymbol = state.info.fromAmountSymbol,
)
ExpressShareImageAddress(
prefix = stringResourceSafe(R.string.common_from),
address = state.info.fromAddress,
)
SpacerH(24.dp)
ExpressShareImageSeparator()
SpacerH(24.dp)
ExpressShareImageAmount(
prefix = stringResourceSafe(R.string.common_receive),
amountValue = state.info.toAmountValue,
currencyIconState = state.info.toCurrencyIcon,
currencySymbol = state.info.toAmountSymbol,
)
ExpressShareImageAddress(
prefix = stringResourceSafe(R.string.common_to),
address = state.info.toAddress,
)
SpacerH(24.dp)
ExpressShareImageProvider(state)
Text(
text = stringResourceSafe(R.string.express_transaction_id, state.info.txExternalId.orEmpty()),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors3.text.staticDark.secondary,
)
}
Image(
painter = painterResource(R.drawable.img_share_express_background),
contentDescription = null,
modifier = Modifier,
contentScale = ContentScale.Crop,
)
}
}
}
}
@Composable
private fun ExpressShareImageAmount(
prefix: String,
amountValue: BigDecimal,
currencyIconState: CurrencyIconState,
currencySymbol: String,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = prefix,
color = TangemTheme.colors3.text.staticDark.primary,
style = TangemTheme.typography2.bodySemibold16,
)
Text(
text = amountValue.stripZeroPlainString(),
color = TangemTheme.colors3.text.staticDark.primary,
style = TangemTheme.typography2.bodySemibold16,
)
CurrencyIcon(
state = currencyIconState,
iconSize = 20.dp,
shouldDisplayNetwork = false,
)
Text(
text = currencySymbol,
color = TangemTheme.colors3.text.staticDark.primary,
style = TangemTheme.typography2.bodySemibold16,
)
}
}
@Composable
private fun ExpressShareImageAddress(prefix: String, address: String?) {
if (address != null) {
Text(
text = "$prefix:",
color = TangemTheme.colors3.text.staticDark.secondary,
)
Text(
text = address,
color = TangemTheme.colors3.text.staticDark.secondary,
)
}
}
@Composable
private fun ExpressShareImageSeparator() {
val width = 1.dp
val height = 39.dp
val dashOnInterval = (width * 2).toPx()
val dashOffInterval = (width * 2).toPx()
val pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(dashOnInterval, dashOffInterval),
)
Canvas(modifier = Modifier.size(width = width, height = height)) {
drawLine(
color = TangemColorPalette.Dark2,
start = Offset(0f, 0f),
end = Offset(size.width, size.height),
strokeWidth = width.toPx(),
cap = StrokeCap.Round,
pathEffect = pathEffect,
)
}
}
@Composable
private fun ExpressShareImageProvider(state: ExpressTransactionStateUM) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = stringResourceSafe(R.string.express_by_provider),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors3.text.staticDark.primary,
)
val providerImageUrl = when (state) {
is ExchangeUM -> state.provider.imageLarge
is ExpressTransactionStateUM.OnrampUM -> state.providerImageUrl
else -> null
}
val providerName = when (state) {
is ExchangeUM -> state.provider.name
is ExpressTransactionStateUM.OnrampUM -> state.providerName
else -> null
}
val providerType = when (state) {
is ExchangeUM -> state.provider.type.providerName
is ExpressTransactionStateUM.OnrampUM -> state.providerType
else -> null
}
if (providerImageUrl != null) {
TangemIcon(
tangemIconUM = TangemIconUM.Url(providerImageUrl, fallbackRes = R.drawable.ic_empty_64),
modifier = Modifier.size(20.dp),
)
}
Text(
text = providerName.orEmpty(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors3.text.staticDark.primary,
)
Text(
text = providerType.orEmpty(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors3.text.staticDark.secondary,
)
}
}
@Composable
private fun Modifier.drawForShare(onGraphicsLayer: (GraphicsLayer) -> Unit): Modifier {
val isInspectionMode = LocalInspectionMode.current
return drawWithCache {
// draw to graphics layer
onGraphicsLayer(
obtainGraphicsLayer().apply {
record(
size = IntSize(
width = 525.dp.toPx().toInt(),
height = 580.dp.toPx().toInt(),
),
) {
drawContent()
}
},
)
if (isInspectionMode) {
// draw only for preview mode
onDrawWithContent { drawContent() }
} else {
// leave blank to skip drawing on the screen
onDrawWithContent { }
}
}
}
@Suppress("MagicNumber")
private suspend fun GraphicsLayer.saveAsShareableFile(context: Context): Uri? {
// convert to bitmap
val bitmap = this.toImageBitmap().asAndroidBitmap()
// create file
val cachePath = File(context.cacheDir, "images")
cachePath.mkdir()
val file = File(cachePath, "shared_image.png")
// write bitmap to file as PNG
file.outputStream().use { out ->
bitmap.compress(/* format = */ Bitmap.CompressFormat.PNG, /* quality = */ 100, /* stream = */ out)
out.flush()
}
// Generate secure Content URI using the registered authority
return FileProvider.getUriForFile(
/* context = */ context,
/* authority = */ "${context.packageName}.provider",
/* file = */ file,
)
}
@ReadOnlyComposable
@Composable
internal fun makeExpressShareContent(state: ExpressTransactionStateUM): String? {
val txExternalId = state.info.txExternalId
val txId = if (txExternalId != null) {
stringResourceSafe(R.string.express_transaction_id, txExternalId)
} else {
""
}
val (providerName, providerType) = when (state) {
is ExchangeUM -> state.provider.name to state.provider.type
is ExpressTransactionStateUM.OnrampUM -> state.providerName to state.providerType
else -> return null
}
val providerInfo = "${stringResourceSafe(R.string.express_by_provider)} $providerName $providerType"
val fromAddress = if (state.info.fromAddress != null) {
"${stringResourceSafe(R.string.common_from)}: ${state.info.fromAddress}"
} else {
""
}
val text = """
${stringResourceSafe(R.string.common_tangem)}
${stringResourceSafe(R.string.common_send)} ${state.info.fromAmount.resolveReference()}
$fromAddress
${stringResourceSafe(R.string.common_receive)} ${state.info.toAmount.resolveReference()}
${stringResourceSafe(R.string.common_to)}: ${state.info.toAddress}
$providerInfo
$txId
""".trimIndent()
return text
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, widthDp = 360, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun ExpressShareImageContent_Preview(
@PreviewParameter(ExpressStatusBottomSheetStateProvider::class) param: ExpressStatusBottomSheetConfig,
) {
TangemThemePreviewRedesign {
ExpressShareImageContent(param.value, {}, modifier = Modifier)
}
}
// endregion

View file

@ -10,10 +10,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.expressStatus.ExpressEstimate import com.tangem.common.ui.expressStatus.ExpressEstimate
import com.tangem.common.ui.expressStatus.ExpressHideButton import com.tangem.common.ui.expressStatus.ExpressHideButton
import com.tangem.common.ui.expressStatus.ExpressProvider import com.tangem.common.ui.expressStatus.ExpressProvider
import com.tangem.core.ui.R import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH16
@ -28,7 +30,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component
import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM
@Composable @Composable
internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM, extraContent: (@Composable () -> Unit)? = null) { internal fun ExchangeStatusBottomSheetContent(
state: ExchangeUM,
isExpressShareButtonEnabled: Boolean,
extraContent: (@Composable () -> Unit)? = null,
) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16) .padding(horizontal = TangemTheme.dimens.spacing16)
@ -87,8 +93,12 @@ internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM, extraContent: (
isAutoDisposable = state.activeStatus?.isAutoDisposable == true, isAutoDisposable = state.activeStatus?.isAutoDisposable == true,
onClick = state.info.onDisposeExpressStatus, onClick = state.info.onDisposeExpressStatus,
) )
if (isExpressShareButtonEnabled) {
SpacerH(80.dp)
} else {
SpacerH24() SpacerH24()
} }
}
} }
@Composable @Composable

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View file

@ -63,6 +63,7 @@ internal class SingleWalletOnrampTransactionConverter(
timestampAgoFormatted = mapFormattedDate(value.timestamp), timestampAgoFormatted = mapFormattedDate(value.timestamp),
activeStatus = value.status.toActiveStatusText(currency.name), activeStatus = value.status.toActiveStatusText(currency.name),
toAmount = stringReference(value.toAmount.format { crypto(currency) }), toAmount = stringReference(value.toAmount.format { crypto(currency) }),
toAmountValue = value.toAmount,
toFiatAmount = stringReference( toFiatAmount = stringReference(
status.fiatRate?.multiply(value.toAmount).format { status.fiatRate?.multiply(value.toAmount).format {
fiat( fiat(
@ -73,6 +74,7 @@ internal class SingleWalletOnrampTransactionConverter(
), ),
toAmountSymbol = currency.symbol, toAmountSymbol = currency.symbol,
toCurrencyIcon = iconStateConverter.convert(currency), toCurrencyIcon = iconStateConverter.convert(currency),
toAddress = status.networkAddress?.defaultAddress?.value.orEmpty(),
fromAmount = stringReference( fromAmount = stringReference(
value.fromAmount.format { value.fromAmount.format {
fiat( fiat(
@ -81,12 +83,14 @@ internal class SingleWalletOnrampTransactionConverter(
) )
}, },
), ),
fromAmountValue = value.fromAmount,
fromFiatAmount = null, fromFiatAmount = null,
fromAmountSymbol = value.fromCurrency.code, fromAmountSymbol = value.fromCurrency.code,
fromCurrencyIcon = CurrencyIconState.FiatIcon( fromCurrencyIcon = CurrencyIconState.FiatIcon(
url = value.fromCurrency.image, url = value.fromCurrency.image,
fallbackResId = R.drawable.ic_currency_24, fallbackResId = R.drawable.ic_currency_24,
), ),
fromAddress = null,
iconState = value.status.toIconState(), iconState = value.status.toIconState(),
onGoToProviderClick = { url -> onGoToProviderClick = { url ->
analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider()) analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider())