Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions Features/MarketInsight/Sources/Scenes/ChartScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public struct ChartScene: View {
public init(model: ChartSceneViewModel) {
_model = State(initialValue: model)
}

public var body: some View {
ChartListView(model: model) {
if model.showPriceAlerts, let asset = model.priceData?.asset {
Expand Down Expand Up @@ -59,26 +59,31 @@ public struct ChartScene: View {
private func marketSection(_ items: [MarketValueViewModel]) -> some View {
Section {
ForEach(items, id: \.title) { item in
if let url = item.url {
SafariNavigationLink(url: url) {
switch item.action {
case .explorer(let explorerContext):
SafariNavigationLink(url: explorerContext.explorerLink.url) {
ListItemView(title: item.title, subtitle: item.subtitle)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency and to ensure all relevant view model properties are displayed, it's better to use the marketItemView helper function here, similar to the .info and .none cases. This will ensure that properties like titleTag, titleExtra, etc., are rendered if they exist on the item.

Suggested change
ListItemView(title: item.title, subtitle: item.subtitle)
marketItemView(item)

}
.contextMenu(
item.value.map { [.copy(value: $0)] } ?? []
)
} else {
ListItemView(
title: item.title,
titleTag: item.titleTag,
titleTagStyle: item.titleTagStyle ?? .body,
titleExtra: item.titleExtra,
subtitle: item.subtitle,
subtitleExtra: item.subtitleExtra,
subtitleStyleExtra: item.subtitleExtraStyle ?? .calloutSecondary,
infoAction: item.infoSheetType.map { type in { model.isPresentingInfoSheet = type } }
)
.explorerContext(explorerContext)
case .info(let type):
marketItemView(item, infoAction: { model.onSelectInfoSheet(type) })
case .none:
marketItemView(item)
}
}
}
}

private func marketItemView(_ item: MarketValueViewModel, infoAction: (() -> Void)? = nil) -> some View {
ListItemView(
title: item.title,
titleTag: item.titleTag,
titleTagStyle: item.titleTagStyle ?? .body,
titleExtra: item.titleExtra,
subtitle: item.subtitle,
subtitleExtra: item.subtitleExtra,
subtitleStyleExtra: item.subtitleExtraStyle ?? .calloutSecondary,
infoAction: infoAction
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,13 @@ struct AssetDetailsInfoViewModel {
MarketValueViewModel(
title: Localized.Asset.contract,
subtitle: contractText,
value: contract,
url: contractUrl
action: contract.flatMap { contract in
contractExplorerLink.map {
MarketValueViewModel.Action.explorer(
ExplorerContextData(copyValue: .address(value: contract, chain: priceData.asset.chain), explorerLink: $0)
)
}
} ?? .none
)
}

Expand All @@ -65,7 +70,7 @@ struct AssetDetailsInfoViewModel {
contract.map { AddressFormatter(address: $0, chain: priceData.asset.chain).value() }
}

private var contractUrl: URL? {
contract.flatMap { explorerService.tokenUrl(chain: priceData.asset.chain, address: $0)?.url }
private var contractExplorerLink: BlockExplorerLink? {
contract.flatMap { explorerService.tokenUrl(chain: priceData.asset.chain, address: $0) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct AssetMarketViewModel {
MarketValueViewModel(
title: Localized.Info.FullyDilutedValuation.title,
subtitle: formatCurrency(market.marketCapFdv),
infoSheetType: .fullyDilutedValuation
action: .info(.fullyDilutedValuation)
)
}

Expand All @@ -58,23 +58,23 @@ struct AssetMarketViewModel {
MarketValueViewModel(
title: Localized.Asset.circulatingSupply,
subtitle: formatSupply(market.circulatingSupply),
infoSheetType: .circulatingSupply
action: .info(.circulatingSupply)
)
}

var totalSupply: MarketValueViewModel {
MarketValueViewModel(
title: Localized.Asset.totalSupply,
subtitle: formatSupply(market.totalSupply),
infoSheetType: .totalSupply
action: .info(.totalSupply)
)
}

var maxSupply: MarketValueViewModel {
MarketValueViewModel(
title: Localized.Info.MaxSupply.title,
subtitle: market.maxSupply == 0 ? "∞ \(assetSymbol)" : formatSupply(market.maxSupply),
infoSheetType: .maxSupply
action: .info(.maxSupply)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,8 @@ extension ChartSceneViewModel {
public func onSelectSetPriceAlerts() {
onSetPriceAlert(assetModel.asset)
}

func onSelectInfoSheet(_ type: InfoSheetType) {
isPresentingInfoSheet = type
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,42 @@ import Foundation
import SwiftUI
import Style
import InfoSheet
import PrimitivesComponents

struct MarketValueViewModel {
enum Action {
case none
case explorer(ExplorerContextData)
case info(InfoSheetType)
}

let title: String
let titleExtra: String?
let subtitle: String?
let subtitleExtra: String?
let subtitleExtraStyle: TextStyle?
let value: String?
let url: URL?
let action: Action
let titleTag: String?
let titleTagStyle: TextStyle?
let infoSheetType: InfoSheetType?

init(
title: String,
titleExtra: String? = .none,
subtitle: String?,
subtitleExtra: String? = .none,
subtitleExtraStyle: TextStyle? = .none,
value: String? = .none,
url: URL? = .none,
action: Action = .none,
titleTag: String? = .none,
titleTagStyle: TextStyle? = .none,
infoSheetType: InfoSheetType? = .none
titleTagStyle: TextStyle? = .none
) {
self.title = title
self.titleExtra = titleExtra
self.subtitle = subtitle
self.subtitleExtra = subtitleExtra
self.subtitleExtraStyle = subtitleExtraStyle
self.value = value
self.url = url
self.action = action
self.titleTag = titleTag
self.titleTagStyle = titleTagStyle
self.infoSheetType = infoSheetType
}
}

Expand Down
22 changes: 15 additions & 7 deletions Features/NFT/Sources/Scenes/CollectibleScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public struct CollectibleScene: View {
}
.alertSheet($model.isPresentingAlertMessage)
.toast(message: $model.isPresentingToast)
.safariSheet(url: $model.isPresentingTokenExplorerUrl)
.sheet(isPresented: $model.isPresentingReportSheet) {
ReportNavigationStack(
model: ReportNftViewModel(
Expand Down Expand Up @@ -102,12 +101,10 @@ extension CollectibleScene {
assetImage: model.networkAssetImage
)

if let contractField = model.contractField {
ListItemView(field: contractField)
.contextMenu(model.contractContextMenu)
if let contractRow = model.contractRow {
infoRowView(contractRow)
}
ListItemView(field: model.tokenIdField)
.contextMenu(model.tokenIdContextMenu)
infoRowView(model.tokenIdRow)
}
}

Expand All @@ -124,5 +121,16 @@ extension CollectibleScene {
SocialLinksView(model: model.socialLinksViewModel)
}
}
}

@ViewBuilder
private func infoRowView(_ row: CollectibleInfoRow) -> some View {
switch row.action {
case .explorer(let explorerContext):
ListItemView(field: row.field)
.explorerContext(explorerContext)
case .copy(let copyValue):
ListItemView(field: row.field)
.contextMenu(.copy(value: copyValue, onCopy: model.onSelectCopyValue))
}
}
}
14 changes: 14 additions & 0 deletions Features/NFT/Sources/Types/CollectibleInfoRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c). Gem Wallet. All rights reserved.

import Components
import PrimitivesComponents

struct CollectibleInfoRow {
enum Action {
case copy(String)
case explorer(ExplorerContextData)
}

let field: ListItemField
let action: Action
}
82 changes: 45 additions & 37 deletions Features/NFT/Sources/ViewModels/CollectibleViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ public final class CollectibleViewModel {

var isPresentingAlertMessage: AlertMessage?
var isPresentingToast: ToastMessage?
var isPresentingTokenExplorerUrl: URL?
var isPresentingSelectedAssetInput: Binding<SelectedAssetInput?>
var isPresentingReportSheet = false
var isPresentingInfoSheet: InfoSheetType?
Expand Down Expand Up @@ -86,27 +85,49 @@ public final class CollectibleViewModel {
return ListItemField(title: Localized.Asset.contract, value: text)
}

var contractContextMenu: [ContextMenuItemType] {
[
.copy(value: contractValue, onCopy: { [weak self] value in
self?.isPresentingToast = .copied(value)
}),
contractExplorerUrl.map {
.url(title: Localized.Transaction.viewOn($0.name), onOpen: onSelectViewContractInExplorer)
}
].compactMap { $0 }
var contractExplorerLink: BlockExplorerLink? {
explorerService.tokenUrl(chain: assetData.asset.chain, address: contractValue)
}

var contractExplorerContext: ExplorerContextData? {
contractExplorerLink.map {
ExplorerContextData(copyValue: .address(value: contractValue, chain: assetData.asset.chain), explorerLink: $0)
}
}

var contractRow: CollectibleInfoRow? {
contractField.map {
CollectibleInfoRow(
field: $0,
action: contractExplorerContext.map { .explorer($0) } ?? .copy(contractValue)
)
}
}

var tokenIdValue: String { assetData.asset.tokenId }
var tokenIdField: ListItemField {
let text = if assetData.asset.tokenId.count > 16 {
assetData.asset.tokenId
AddressFormatter(address: assetData.asset.tokenId, chain: assetData.asset.chain).value()
} else {
"#\(assetData.asset.tokenId)"
}
return ListItemField(title: Localized.Asset.tokenId, value: text)
}

var tokenIdExplorerLink: BlockExplorerLink? {
explorerService.nftUrl(
chain: assetData.asset.chain,
contractAddress: contractValue,
tokenId: tokenIdValue
)
}

var tokenIdExplorerContext: ExplorerContextData? {
tokenIdExplorerLink.map {
ExplorerContextData(copyValue: .plain(tokenIdValue), explorerLink: $0)
}
}

var attributesTitle: String { Localized.Nft.properties }
var attributes: [NFTAttribute] { assetData.asset.attributes }

Expand Down Expand Up @@ -169,27 +190,25 @@ public final class CollectibleViewModel {
SocialLinksViewModel(assetLinks: assetData.collection.links)
}

var tokenExplorerUrl: BlockExplorerLink? {
explorerService.tokenUrl(chain: assetData.asset.chain, address: assetData.asset.tokenId)
}

var tokenIdContextMenu: [ContextMenuItemType] {
let items: [ContextMenuItemType] = [
.copy(value: tokenIdValue, onCopy: { [weak self] value in
self?.isPresentingToast = .copied(value)
}),
tokenExplorerUrl.map {
.url(title: Localized.Transaction.viewOn($0.name), onOpen: onSelectViewTokenInExplorer)
}
].compactMap { $0 }

return items
var tokenIdRow: CollectibleInfoRow {
CollectibleInfoRow(
field: tokenIdField,
action: tokenIdExplorerContext.map { .explorer($0) } ?? .copy(tokenIdValue)
)
}
}

// MARK: - Business Logic

extension CollectibleViewModel {
func onSelectCopyValue(_ value: CopyValue) {
isPresentingToast = .copied(value.displayValue)
}

func onSelectCopyValue(_ value: String) {
isPresentingToast = .copied(value)
}

func onSelectHeaderButton(type: HeaderButtonType) {
guard let account = try? wallet.account(for: assetData.asset.chain) else {
return
Expand Down Expand Up @@ -247,14 +266,6 @@ extension CollectibleViewModel {
}
}

func onSelectViewTokenInExplorer() {
isPresentingTokenExplorerUrl = tokenExplorerUrl?.url
}

func onSelectViewContractInExplorer() {
isPresentingTokenExplorerUrl = contractExplorerUrl?.url
}

func onSelectReport() {
isPresentingReportSheet = true
}
Expand All @@ -274,9 +285,6 @@ extension CollectibleViewModel {
extension CollectibleViewModel {
private static let enabledChainTypes: Set<ChainType> = [.ethereum]
private var contractValue: String { assetData.collection.contractAddress }
private var contractExplorerUrl: BlockExplorerLink? {
explorerService.tokenUrl(chain: assetData.asset.chain, address: contractValue)
}

private func openSettings() {
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }
Expand Down
Loading
Loading