From b127951178df1f31815930bfaaeab723863528f1 Mon Sep 17 00:00:00 2001 From: Radmir Date: Mon, 6 Apr 2026 18:43:00 +0500 Subject: [PATCH] Add address context menu with copy and explorer link support Introduce reusable AddressPropertyItem component with long-press context menu (copy address, view on explorer). Apply it to transaction details, confirm screen, asset chart contract, NFT details, and wallet address. --- .../transaction/GetTransactionDetailsImpl.kt | 13 +++- .../components/DestinationPropertyItem.kt | 70 +++++------------- .../asset/presents/chart/AssetChartScene.kt | 49 +++++------- .../chart/models/MarketInfoUIModel.kt | 2 + .../components/PropertyDestination.kt | 74 ++++++++++++------- .../confirm/models/ConfirmProperty.kt | 3 +- .../confirm/viewmodels/ConfirmViewModel.kt | 20 ++++- .../features/nft/presents/NftDetailsScene.kt | 25 +++++-- .../nft/viewmodels/NftDetailsViewModel.kt | 26 ++++++- .../presents/components/WalletAddress.kt | 66 ++--------------- .../values/TransactionDetailsValue.kt | 7 +- .../list_item/property/AddressPropertyItem.kt | 72 ++++++++++++++++++ 12 files changed, 243 insertions(+), 184 deletions(-) create mode 100644 android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/AddressPropertyItem.kt diff --git a/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/transaction/GetTransactionDetailsImpl.kt b/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/transaction/GetTransactionDetailsImpl.kt index 67173a6c3..7c845b10c 100644 --- a/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/transaction/GetTransactionDetailsImpl.kt +++ b/android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/transaction/GetTransactionDetailsImpl.kt @@ -17,12 +17,15 @@ import com.gemwallet.android.model.AssetInfo import com.gemwallet.android.model.Crypto import com.gemwallet.android.model.TransactionExtended import com.gemwallet.android.model.format +import com.gemwallet.android.domains.asset.chain import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.BlockExplorerLink import com.wallet.core.primitives.Currency import com.wallet.core.primitives.TransactionDirection import com.wallet.core.primitives.TransactionState import com.wallet.core.primitives.TransactionSwapMetadata import com.wallet.core.primitives.TransactionType +import uniffi.gemstone.Explorer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -54,6 +57,8 @@ class GetTransactionDetailsImpl( val explorerInfo = getCurrentBlockExplorer.getBlockExplorerInfo(data.transaction).let { (url, name) -> TransactionDetailsValue.Explorer(url, name) } + val chainExplorer = Explorer(data.asset.chain.string) + val explorerName = explorerInfo.name assetsRepository.getAssetsInfo(ids).mapLatest { assets -> val swapMetadata = data.transaction.getSwapMetadata() val provider = gemSwapper.getProviders().firstOrNull { it.protocolId == swapMetadata?.provider } @@ -64,6 +69,8 @@ class GetTransactionDetailsImpl( currency = session.currency, swapProvider = provider, swapMetadata = swapMetadata, + senderExplorerLink = BlockExplorerLink(explorerName, chainExplorer.getAddressUrl(explorerName, data.transaction.from)), + recipientExplorerLink = BlockExplorerLink(explorerName, chainExplorer.getAddressUrl(explorerName, data.transaction.to)), ) } } @@ -79,6 +86,8 @@ class TransactionDetailsAggregateImpl( override val explorer: TransactionDetailsValue.Explorer, override val currency: Currency, swapProvider: SwapperProviderType? = null, + private val senderExplorerLink: BlockExplorerLink? = null, + private val recipientExplorerLink: BlockExplorerLink? = null, ) : TransactionDetailsAggregate { override val id: String = data.transaction.id @@ -186,8 +195,8 @@ class TransactionDetailsAggregateImpl( TransactionType.Transfer, TransactionType.TransferNFT -> when (data.transaction.direction) { TransactionDirection.SelfTransfer, - TransactionDirection.Outgoing -> TransactionDetailsValue.Destination.Recipient(data.transaction.to) - TransactionDirection.Incoming -> TransactionDetailsValue.Destination.Sender(data.transaction.from) + TransactionDirection.Outgoing -> TransactionDetailsValue.Destination.Recipient(data.transaction.to, recipientExplorerLink) + TransactionDirection.Incoming -> TransactionDetailsValue.Destination.Sender(data.transaction.from, senderExplorerLink) } } diff --git a/android/features/activities/presents/src/main/kotlin/com/gemwallet/android/features/activities/presents/details/components/DestinationPropertyItem.kt b/android/features/activities/presents/src/main/kotlin/com/gemwallet/android/features/activities/presents/details/components/DestinationPropertyItem.kt index 154e74d20..a09fd2520 100644 --- a/android/features/activities/presents/src/main/kotlin/com/gemwallet/android/features/activities/presents/details/components/DestinationPropertyItem.kt +++ b/android/features/activities/presents/src/main/kotlin/com/gemwallet/android/features/activities/presents/details/components/DestinationPropertyItem.kt @@ -1,66 +1,34 @@ package com.gemwallet.android.features.activities.presents.details.components -import androidx.compose.foundation.clickable -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext -import androidx.compose.foundation.layout.padding import com.gemwallet.android.ext.AddressFormatter import com.gemwallet.android.domains.transaction.values.TransactionDetailsValue import com.gemwallet.android.ui.R -import com.gemwallet.android.ui.components.clipboard.setPlainText -import com.gemwallet.android.ui.components.list_item.property.PropertyDataText +import com.gemwallet.android.ui.components.list_item.property.AddressPropertyItem import com.gemwallet.android.ui.components.list_item.property.PropertyItem import com.gemwallet.android.ui.components.list_item.property.PropertyTitleText +import com.gemwallet.android.ui.components.list_item.property.PropertyDataText import com.gemwallet.android.ui.models.ListPosition -import com.gemwallet.android.ui.theme.paddingSmall @Composable fun DestinationPropertyItem(property: TransactionDetailsValue.Destination, listPosition: ListPosition) { - val context = LocalContext.current - val clipboardManager = LocalClipboard.current.nativeClipboard - val title = when (property) { - is TransactionDetailsValue.Destination.Recipient -> R.string.transaction_recipient - is TransactionDetailsValue.Destination.Sender -> R.string.transaction_sender - is TransactionDetailsValue.Destination.Provider -> R.string.common_provider - } - val isCopied = when (property) { - is TransactionDetailsValue.Destination.Recipient, - is TransactionDetailsValue.Destination.Sender -> true - is TransactionDetailsValue.Destination.Provider -> false - } - - val displayData = when (property) { + when (property) { is TransactionDetailsValue.Destination.Recipient, - is TransactionDetailsValue.Destination.Sender -> AddressFormatter(property.data).value() - is TransactionDetailsValue.Destination.Provider -> property.data + is TransactionDetailsValue.Destination.Sender -> AddressPropertyItem( + title = when (property) { + is TransactionDetailsValue.Destination.Recipient -> R.string.transaction_recipient + is TransactionDetailsValue.Destination.Sender -> R.string.transaction_sender + else -> return + }, + displayText = AddressFormatter(property.data).value(), + copyValue = property.data, + explorerLink = property.explorerLink, + listPosition = listPosition, + ) + is TransactionDetailsValue.Destination.Provider -> PropertyItem( + title = { PropertyTitleText(R.string.common_provider) }, + data = { PropertyDataText(text = property.data) }, + listPosition = listPosition, + ) } - PropertyItem( - title = { PropertyTitleText(title) }, - data = { - PropertyDataText( - text = displayData, - modifier = Modifier - .clickable(enabled = isCopied) { clipboardManager.setPlainText(context, property.data) }, - badge = if (isCopied) { - { - Icon( - modifier = Modifier.padding(start = paddingSmall), - imageVector = Icons.Default.ContentCopy, - tint = MaterialTheme.colorScheme.secondary, - contentDescription = null, - ) - } - } else { - null - } - ) - }, - listPosition = listPosition, - ) } diff --git a/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/chart/AssetChartScene.kt b/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/chart/AssetChartScene.kt index ab5c4d0a3..b893cd42a 100644 --- a/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/chart/AssetChartScene.kt +++ b/android/features/asset/presents/src/main/kotlin/com/gemwallet/android/features/asset/presents/chart/AssetChartScene.kt @@ -1,9 +1,7 @@ package com.gemwallet.android.features.asset.presents.chart import androidx.compose.foundation.clickable -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.foundation.combinedClickable +import com.gemwallet.android.ext.AddressFormatter import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.lazy.LazyColumn @@ -17,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.testTag @@ -28,12 +25,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.gemwallet.android.domains.asset.chain import com.gemwallet.android.domains.percentage.formatAsPercentage import com.gemwallet.android.domains.price.toPriceState -import com.gemwallet.android.ext.AddressFormatter import com.gemwallet.android.model.compactFormatter import com.gemwallet.android.model.formatSupply import com.gemwallet.android.ui.R import com.gemwallet.android.ui.components.InfoSheetEntity -import com.gemwallet.android.ui.components.clipboard.setPlainText import com.gemwallet.android.ui.components.image.AsyncImage import com.gemwallet.android.ui.components.list_item.ChipBadge import com.gemwallet.android.ui.components.list_item.ListItem @@ -41,6 +36,7 @@ import com.gemwallet.android.ui.components.list_item.ListItemSupportText import com.gemwallet.android.ui.components.list_item.ListItemTitleText import com.gemwallet.android.ui.components.list_item.SubheaderItem import com.gemwallet.android.ui.components.list_item.color +import com.gemwallet.android.ui.components.list_item.property.AddressPropertyItem import com.gemwallet.android.ui.components.list_item.property.DataBadgeChevron import com.gemwallet.android.ui.components.list_item.property.PropertyDataText import com.gemwallet.android.ui.components.list_item.property.PropertyItem @@ -59,6 +55,7 @@ import com.gemwallet.android.features.asset.viewmodels.chart.viewmodels.ChartVie import com.wallet.core.primitives.Asset import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetMarket +import com.wallet.core.primitives.BlockExplorerLink import com.wallet.core.primitives.Currency import uniffi.gemstone.Explorer import java.text.DateFormat @@ -176,12 +173,6 @@ private fun LazyListScope.links(links: List) { private fun LazyListScope.assetMarket(currency: Currency, asset: Asset, marketInfo: AssetMarket?, explorerName: String) { marketInfo ?: return val marketItems = listOfNotNull( - asset.id.tokenId?.let { - MarketInfoUIModel( - type = MarketInfoUIModel.MarketInfoTypeUIModel.Contract, - value = it, - ) - }, marketInfo.marketCap?.let { MarketInfoUIModel( type = MarketInfoUIModel.MarketInfoTypeUIModel.MarketCap, @@ -226,6 +217,14 @@ private fun LazyListScope.assetMarket(currency: Currency, asset: Asset, marketIn info = InfoSheetEntity.MaxSupply, ) }, + asset.id.tokenId?.let { tokenId -> + MarketInfoUIModel( + type = MarketInfoUIModel.MarketInfoTypeUIModel.Contract, + value = tokenId, + explorerLink = Explorer(asset.chain.string).getTokenUrl(explorerName, tokenId) + ?.let { BlockExplorerLink(name = explorerName, link = it) }, + ) + }, ) val allTime = listOfNotNull( @@ -257,26 +256,12 @@ private fun LazyListScope.marketProperties(asset: Asset, explorerName: String, i listPosition = position ) MarketInfoUIModel.MarketInfoTypeUIModel.Contract -> { - val context = LocalContext.current - val clipboardManager = LocalClipboard.current.nativeClipboard - val uriHandler = LocalUriHandler.current - PropertyItem( - modifier = Modifier.combinedClickable( - onLongClick = { - clipboardManager.setPlainText(context, item.value) - }, - onClick = { - uriHandler.open(context, Explorer(asset.chain.string).getTokenUrl(explorerName, item.value) ?: return@combinedClickable) - } - ), - title = { PropertyTitleText(R.string.asset_contract) }, - data = { - PropertyDataText( - text = AddressFormatter(item.value, chain = asset.chain).value(), - badge = { DataBadgeChevron() } - ) - }, - listPosition = position + AddressPropertyItem( + title = R.string.asset_contract, + displayText = AddressFormatter(item.value, chain = asset.chain).value(), + copyValue = item.value, + explorerLink = item.explorerLink, + listPosition = position, ) } } diff --git a/android/features/asset/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset/viewmodels/chart/models/MarketInfoUIModel.kt b/android/features/asset/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset/viewmodels/chart/models/MarketInfoUIModel.kt index f45d26f53..2e3f5fab8 100644 --- a/android/features/asset/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset/viewmodels/chart/models/MarketInfoUIModel.kt +++ b/android/features/asset/viewmodels/src/main/kotlin/com/gemwallet/android/features/asset/viewmodels/chart/models/MarketInfoUIModel.kt @@ -3,12 +3,14 @@ package com.gemwallet.android.features.asset.viewmodels.chart.models import androidx.annotation.StringRes import com.gemwallet.android.ui.R import com.gemwallet.android.ui.components.InfoSheetEntity +import com.wallet.core.primitives.BlockExplorerLink class MarketInfoUIModel( val type: MarketInfoTypeUIModel, val value: String, val badge: String? = null, val info: InfoSheetEntity? = null, + val explorerLink: BlockExplorerLink? = null, ) { enum class MarketInfoTypeUIModel(@param:StringRes val label: Int) { MarketCap(R.string.asset_market_cap), diff --git a/android/features/confirm/presents/src/main/kotlin/com/gemwallet/android/features/confirm/presents/components/PropertyDestination.kt b/android/features/confirm/presents/src/main/kotlin/com/gemwallet/android/features/confirm/presents/components/PropertyDestination.kt index 24d9e5f13..7f9d15716 100644 --- a/android/features/confirm/presents/src/main/kotlin/com/gemwallet/android/features/confirm/presents/components/PropertyDestination.kt +++ b/android/features/confirm/presents/src/main/kotlin/com/gemwallet/android/features/confirm/presents/components/PropertyDestination.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import com.gemwallet.android.ext.AddressFormatter import com.gemwallet.android.ui.R +import com.gemwallet.android.ui.components.list_item.property.AddressPropertyItem import com.gemwallet.android.ui.components.list_item.property.PropertyDataText import com.gemwallet.android.ui.components.list_item.property.PropertyItem import com.gemwallet.android.ui.components.list_item.property.PropertyTitleText @@ -20,32 +21,53 @@ fun PropertyDestination( ) { model ?: return - val title = when (model) { - is ConfirmProperty.Destination.Provider -> R.string.common_provider - is ConfirmProperty.Destination.Stake -> R.string.stake_validator - is ConfirmProperty.Destination.Transfer -> R.string.transaction_recipient - is ConfirmProperty.Destination.Generic -> R.string.wallet_connect_app - is ConfirmProperty.Destination.PerpetualOper -> R.string.common_provider - } - PropertyItem( - title = { - PropertyTitleText(title) - }, - data = { - Column(horizontalAlignment = Alignment.End) { - Row(horizontalArrangement = Arrangement.End) { PropertyDataText(model.displayData()) } + when (model) { + is ConfirmProperty.Destination.Transfer -> { + val domain = model.domain + if (domain != null) { + PropertyItem( + title = { PropertyTitleText(R.string.transaction_recipient) }, + data = { + Column(horizontalAlignment = Alignment.End) { + Row(horizontalArrangement = Arrangement.End) { PropertyDataText(domain) } + } + }, + listPosition = listPosition, + ) + } else { + AddressPropertyItem( + title = R.string.transaction_recipient, + displayText = AddressFormatter(model.address).value(), + copyValue = model.address, + explorerLink = model.explorerLink, + listPosition = listPosition, + ) } - }, - listPosition = listPosition, - ) -} - -internal fun ConfirmProperty.Destination.displayData(): String { - return when (this) { - is ConfirmProperty.Destination.Stake, - is ConfirmProperty.Destination.Provider -> data - is ConfirmProperty.Destination.Transfer -> domain ?: AddressFormatter(address).value() - is ConfirmProperty.Destination.Generic -> appName - is ConfirmProperty.Destination.PerpetualOper -> providerName + } + else -> { + val title = when (model) { + is ConfirmProperty.Destination.Provider -> R.string.common_provider + is ConfirmProperty.Destination.Stake -> R.string.stake_validator + is ConfirmProperty.Destination.Generic -> R.string.wallet_connect_app + is ConfirmProperty.Destination.PerpetualOper -> R.string.common_provider + is ConfirmProperty.Destination.Transfer -> return + } + val text = when (model) { + is ConfirmProperty.Destination.Provider, + is ConfirmProperty.Destination.Stake -> AddressFormatter(model.data).value() + is ConfirmProperty.Destination.Generic -> model.appName + is ConfirmProperty.Destination.PerpetualOper -> model.providerName + is ConfirmProperty.Destination.Transfer -> return + } + PropertyItem( + title = { PropertyTitleText(title) }, + data = { + Column(horizontalAlignment = Alignment.End) { + Row(horizontalArrangement = Arrangement.End) { PropertyDataText(text) } + } + }, + listPosition = listPosition, + ) + } } } diff --git a/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/models/ConfirmProperty.kt b/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/models/ConfirmProperty.kt index 7f3cf1fc3..04cb8710c 100644 --- a/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/models/ConfirmProperty.kt +++ b/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/models/ConfirmProperty.kt @@ -2,6 +2,7 @@ package com.gemwallet.android.features.confirm.models import com.gemwallet.android.model.ConfirmParams import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.BlockExplorerLink import com.wallet.core.primitives.DelegationValidator sealed interface ConfirmProperty { @@ -16,7 +17,7 @@ sealed interface ConfirmProperty { class Provider(data: String) : Destination(data) - class Transfer(val domain: String?, val address: String) : Destination(address) + class Transfer(val domain: String?, val address: String, val explorerLink: BlockExplorerLink? = null) : Destination(address) class Generic(val appName: String) : Destination(appName) diff --git a/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt b/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt index 7aa9131b1..4f3edf0f5 100644 --- a/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt +++ b/android/features/confirm/viewmodels/src/main/kotlin/com/gemwallet/android/features/confirm/viewmodels/ConfirmViewModel.kt @@ -8,6 +8,7 @@ import com.gemwallet.android.blockchain.operators.LoadPrivateKeyOperator import com.gemwallet.android.blockchain.services.BroadcastService import com.gemwallet.android.blockchain.services.SignClientProxy import com.gemwallet.android.blockchain.services.SignerPreloaderProxy +import com.gemwallet.android.cases.nodes.GetCurrentBlockExplorer import com.gemwallet.android.cases.transactions.CreateTransaction import com.gemwallet.android.data.repositories.assets.AssetsRepository import com.gemwallet.android.data.repositories.session.SessionRepository @@ -48,6 +49,7 @@ import com.gemwallet.android.features.confirm.models.ConfirmState import com.gemwallet.android.features.confirm.models.FeeUIModel import com.wallet.core.primitives.AssetId import com.wallet.core.primitives.AssetType +import com.wallet.core.primitives.BlockExplorerLink import com.wallet.core.primitives.Currency import com.wallet.core.primitives.DelegationValidator import com.wallet.core.primitives.FeePriority @@ -77,6 +79,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import com.gemwallet.android.ext.getMinimumAccountBalance import com.wallet.core.primitives.SimulationResult +import uniffi.gemstone.Explorer import java.math.BigInteger import java.util.Arrays import javax.inject.Inject @@ -97,6 +100,7 @@ class ConfirmViewModel @Inject constructor( private val createTransactionsCase: CreateTransaction, private val stakeRepository: StakeRepository, private val transactionBalanceService: TransactionBalanceService, + private val getCurrentBlockExplorer: GetCurrentBlockExplorer, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -243,9 +247,23 @@ class ConfirmViewModel @Inject constructor( val txProperties = combine(request, assetsInfo) { request, assetsInfo -> request ?: return@combine emptyList() val assetInfo = assetsInfo?.getByAssetId(request.assetId) ?: return@combine emptyList() + val chain = assetInfo.asset.id.chain + val explorerName = getCurrentBlockExplorer.getCurrentBlockExplorer(chain) + val chainExplorer = Explorer(chain.string) mutableListOf().apply { add(ConfirmProperty.Source(assetInfo.walletName)) - add(ConfirmProperty.Destination.map(request, getValidator(request))) + val destination = ConfirmProperty.Destination.map(request, getValidator(request)) + add( + if (destination is ConfirmProperty.Destination.Transfer) { + ConfirmProperty.Destination.Transfer( + domain = destination.domain, + address = destination.address, + explorerLink = BlockExplorerLink(explorerName, chainExplorer.getAddressUrl(explorerName, destination.address)), + ) + } else { + destination + } + ) add(request.memo()?.takeIf { (request is ConfirmParams.TransferParams.Native || request is ConfirmParams.TransferParams.Token) && assetInfo.asset.isMemoSupport() diff --git a/android/features/nft/presents/src/main/kotlin/com/gemwallet/android/features/nft/presents/NftDetailsScene.kt b/android/features/nft/presents/src/main/kotlin/com/gemwallet/android/features/nft/presents/NftDetailsScene.kt index 6b9c9529f..9d4484a2f 100644 --- a/android/features/nft/presents/src/main/kotlin/com/gemwallet/android/features/nft/presents/NftDetailsScene.kt +++ b/android/features/nft/presents/src/main/kotlin/com/gemwallet/android/features/nft/presents/NftDetailsScene.kt @@ -23,6 +23,7 @@ import com.gemwallet.android.ext.linkType import com.gemwallet.android.ui.R import com.gemwallet.android.ui.components.image.AsyncImage import com.gemwallet.android.ui.components.list_item.SubheaderItem +import com.gemwallet.android.ui.components.list_item.property.AddressPropertyItem import com.gemwallet.android.ui.components.list_item.property.PropertyItem import com.gemwallet.android.ui.components.list_item.property.PropertyNetworkItem import com.gemwallet.android.ui.components.list_item.property.itemsPositioned @@ -90,15 +91,27 @@ private fun LazyListScope.generalInfo(model: NftAssetDetailsUIModel) { PropertyItem(R.string.nft_collection, model.collection.name, listPosition = ListPosition.First) PropertyNetworkItem(model.collection.chain, listPosition = ListPosition.Middle) model.asset.contractAddress?.let { - val text = AddressFormatter(it, chain = model.collection.chain).value() - PropertyItem(R.string.asset_contract, text, listPosition = ListPosition.Middle) + AddressPropertyItem( + title = R.string.asset_contract, + displayText = AddressFormatter(it, chain = model.collection.chain).value(), + copyValue = it, + explorerLink = model.contractExplorerLink, + listPosition = ListPosition.Middle, + ) } - val tokenIdText = if (model.asset.tokenId.length > 16) { - AddressFormatter(model.asset.tokenId, chain = model.collection.chain).value() + val tokenId = model.asset.tokenId + val tokenIdDisplayText = if (tokenId.length > 16) { + AddressFormatter(tokenId, chain = model.collection.chain).value() } else { - "#${model.asset.tokenId}" + "#$tokenId" } - PropertyItem(R.string.asset_token_id, tokenIdText, listPosition = ListPosition.Last) + AddressPropertyItem( + title = R.string.asset_token_id, + displayText = tokenIdDisplayText, + copyValue = tokenId, + explorerLink = model.tokenIdExplorerLink, + listPosition = ListPosition.Last, + ) } } diff --git a/android/features/nft/viewmodels/src/main/kotlin/com/gemwallet/android/features/nft/viewmodels/NftDetailsViewModel.kt b/android/features/nft/viewmodels/src/main/kotlin/com/gemwallet/android/features/nft/viewmodels/NftDetailsViewModel.kt index 031aff6e1..caa82da8b 100644 --- a/android/features/nft/viewmodels/src/main/kotlin/com/gemwallet/android/features/nft/viewmodels/NftDetailsViewModel.kt +++ b/android/features/nft/viewmodels/src/main/kotlin/com/gemwallet/android/features/nft/viewmodels/NftDetailsViewModel.kt @@ -4,12 +4,15 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.gemwallet.android.cases.nft.GetAssetNft +import com.gemwallet.android.cases.nodes.GetCurrentBlockExplorer import com.gemwallet.android.data.repositories.session.SessionRepository import com.gemwallet.android.ext.getAccount import com.wallet.core.primitives.Account +import com.wallet.core.primitives.BlockExplorerLink import com.wallet.core.primitives.NFTAsset import com.wallet.core.primitives.NFTAttribute import com.wallet.core.primitives.NFTCollection +import uniffi.gemstone.Explorer import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -30,6 +33,7 @@ val nftAssetIdArg = "assetId" class NftDetailsViewModel @Inject constructor( sessionRepository: SessionRepository, private val getAssetNft: GetAssetNft, + private val getCurrentBlockExplorer: GetCurrentBlockExplorer, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -45,7 +49,25 @@ class NftDetailsViewModel @Inject constructor( val (session, assetId) = it getAssetNft.getAssetNft(it.second) .filterNotNull() - .map { NftAssetDetailsUIModel(it.collection, it.assets.first(), session?.wallet?.getAccount(it.assets.first().chain)!!) } + .map { + val nftAsset = it.assets.first() + val chain = nftAsset.chain + val explorerName = getCurrentBlockExplorer.getCurrentBlockExplorer(chain) + val chainExplorer = Explorer(chain.string) + NftAssetDetailsUIModel( + collection = it.collection, + asset = nftAsset, + account = session?.wallet?.getAccount(chain)!!, + contractExplorerLink = nftAsset.contractAddress?.let { address -> + chainExplorer.getTokenUrl(explorerName, address) + ?.let { url -> BlockExplorerLink(explorerName, url) } + }, + tokenIdExplorerLink = nftAsset.contractAddress?.let { address -> + chainExplorer.getNftUrl(explorerName, address, nftAsset.tokenId) + ?.let { url -> BlockExplorerLink(explorerName, url) } + }, + ) + } } .catch { } .flowOn(Dispatchers.IO) @@ -56,6 +78,8 @@ class NftAssetDetailsUIModel( val collection: NFTCollection, val asset: NFTAsset, val account: Account, + val contractExplorerLink: BlockExplorerLink? = null, + val tokenIdExplorerLink: BlockExplorerLink? = null, ) { val imageUrl: String get() = asset.images.preview.url val assetName: String get() = asset.name diff --git a/android/features/wallet-details/presents/src/main/kotlin/com/gemwallet/android/features/wallet/presents/components/WalletAddress.kt b/android/features/wallet-details/presents/src/main/kotlin/com/gemwallet/android/features/wallet/presents/components/WalletAddress.kt index ad11feb4d..62ec03538 100644 --- a/android/features/wallet-details/presents/src/main/kotlin/com/gemwallet/android/features/wallet/presents/components/WalletAddress.kt +++ b/android/features/wallet-details/presents/src/main/kotlin/com/gemwallet/android/features/wallet/presents/components/WalletAddress.kt @@ -1,35 +1,10 @@ package com.gemwallet.android.features.wallet.presents.components -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.DpOffset import com.gemwallet.android.ext.AddressFormatter import com.gemwallet.android.ui.R -import com.gemwallet.android.ui.components.clipboard.setPlainText -import com.gemwallet.android.ui.components.list_item.property.PropertyDataText -import com.gemwallet.android.ui.components.list_item.property.PropertyItem -import com.gemwallet.android.ui.components.list_item.property.PropertyTitleText +import com.gemwallet.android.ui.components.list_item.property.AddressPropertyItem import com.gemwallet.android.ui.models.ListPosition -import com.gemwallet.android.ui.theme.paddingDefault -import com.gemwallet.android.ui.theme.paddingSmall @Composable internal fun WalletAddress( @@ -38,41 +13,10 @@ internal fun WalletAddress( // Show if single account wallet val address = addresses.takeIf { it.size == 1 }?.firstOrNull() ?: return - var isDropDownShow by remember { mutableStateOf(false) } - val clipboardManager = LocalClipboard.current.nativeClipboard - val context = LocalContext.current - - PropertyItem( - modifier = Modifier.combinedClickable( - enabled = true, - onClick = {}, - onLongClick = { isDropDownShow = true } - ), - title = { PropertyTitleText(R.string.common_address) }, - data = { - PropertyDataText( - text = AddressFormatter(address).value(), - ) - }, + AddressPropertyItem( + title = R.string.common_address, + displayText = AddressFormatter(address).value(), + copyValue = address, listPosition = ListPosition.Single, ) - - Box(modifier = Modifier.fillMaxWidth()) { - DropdownMenu( - modifier = Modifier.align(Alignment.BottomEnd), - expanded = isDropDownShow, - offset = DpOffset(paddingDefault, paddingSmall), - containerColor = MaterialTheme.colorScheme.background, - onDismissRequest = { isDropDownShow = false }, - ) { - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.wallet_copy_address)) }, - trailingIcon = { Icon(Icons.Default.ContentCopy, "copy") }, - onClick = { - isDropDownShow = false - clipboardManager.setPlainText(context, address) - }, - ) - } - } } diff --git a/android/gemcore/src/main/kotlin/com/gemwallet/android/domains/transaction/values/TransactionDetailsValue.kt b/android/gemcore/src/main/kotlin/com/gemwallet/android/domains/transaction/values/TransactionDetailsValue.kt index d3c8be915..85b1e6650 100644 --- a/android/gemcore/src/main/kotlin/com/gemwallet/android/domains/transaction/values/TransactionDetailsValue.kt +++ b/android/gemcore/src/main/kotlin/com/gemwallet/android/domains/transaction/values/TransactionDetailsValue.kt @@ -2,6 +2,7 @@ package com.gemwallet.android.domains.transaction.values import com.gemwallet.android.model.AssetInfo import com.wallet.core.primitives.Asset +import com.wallet.core.primitives.BlockExplorerLink import com.wallet.core.primitives.Currency import com.wallet.core.primitives.TransactionNFTTransferMetadata import com.wallet.core.primitives.TransactionState @@ -36,9 +37,9 @@ sealed interface TransactionDetailsValue { class Date(val data: String) : TransactionDetailsValue - sealed class Destination(val data: String) : TransactionDetailsValue { - class Sender(data: String) : Destination(data) - class Recipient(data: String) : Destination(data) + sealed class Destination(val data: String, open val explorerLink: BlockExplorerLink? = null) : TransactionDetailsValue { + class Sender(data: String, override val explorerLink: BlockExplorerLink? = null) : Destination(data, explorerLink) + class Recipient(data: String, override val explorerLink: BlockExplorerLink? = null) : Destination(data, explorerLink) class Provider(name: String) : Destination(name) } diff --git a/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/AddressPropertyItem.kt b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/AddressPropertyItem.kt new file mode 100644 index 000000000..5f4e64b4b --- /dev/null +++ b/android/ui/src/main/kotlin/com/gemwallet/android/ui/components/list_item/property/AddressPropertyItem.kt @@ -0,0 +1,72 @@ +package com.gemwallet.android.ui.components.list_item.property + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import com.gemwallet.android.ui.R +import com.gemwallet.android.ui.components.clipboard.setPlainText +import com.gemwallet.android.ui.components.list_item.DropDownContextItem +import com.gemwallet.android.ui.models.ListPosition +import com.gemwallet.android.ui.open +import com.wallet.core.primitives.BlockExplorerLink + +@Composable +fun AddressPropertyItem( + @StringRes title: Int, + displayText: String, + copyValue: String = displayText, + explorerLink: BlockExplorerLink? = null, + listPosition: ListPosition = ListPosition.Middle, +) { + var isExpanded by remember { mutableStateOf(false) } + val clipboardManager = LocalClipboard.current.nativeClipboard + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + + DropDownContextItem( + isExpanded = isExpanded, + onDismiss = { isExpanded = false }, + onLongClick = { isExpanded = true }, + onClick = {}, + content = { modifier -> + PropertyItem( + modifier = modifier, + title = { PropertyTitleText(title) }, + data = { PropertyDataText(text = displayText) }, + listPosition = listPosition, + ) + }, + menuItems = { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.wallet_copy_address)) }, + trailingIcon = { Icon(Icons.Default.ContentCopy, contentDescription = null) }, + onClick = { + isExpanded = false + clipboardManager.setPlainText(context, copyValue) + }, + ) + if (explorerLink != null) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.transaction_view_on, explorerLink.name)) }, + trailingIcon = { DataBadgeChevron() }, + onClick = { + isExpanded = false + uriHandler.open(context, explorerLink.link) + }, + ) + } + }, + ) +}