Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit c014a17

Browse files
authored
Add explorer context actions (#1826)
* Add explorer context actions * Add explorer context actions * Use NFT explorers for collectible links and shorten long token IDs * Revert NFT explorer links to block explorers
1 parent 315cee8 commit c014a17

21 files changed

Lines changed: 245 additions & 153 deletions

File tree

Features/MarketInsight/Sources/Scenes/ChartScene.swift

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public struct ChartScene: View {
1414
public init(model: ChartSceneViewModel) {
1515
_model = State(initialValue: model)
1616
}
17-
17+
1818
public var body: some View {
1919
ChartListView(model: model) {
2020
if model.showPriceAlerts, let asset = model.priceData?.asset {
@@ -59,26 +59,31 @@ public struct ChartScene: View {
5959
private func marketSection(_ items: [MarketValueViewModel]) -> some View {
6060
Section {
6161
ForEach(items, id: \.title) { item in
62-
if let url = item.url {
63-
SafariNavigationLink(url: url) {
62+
switch item.action {
63+
case .explorer(let explorerContext):
64+
SafariNavigationLink(url: explorerContext.explorerLink.url) {
6465
ListItemView(title: item.title, subtitle: item.subtitle)
6566
}
66-
.contextMenu(
67-
item.value.map { [.copy(value: $0)] } ?? []
68-
)
69-
} else {
70-
ListItemView(
71-
title: item.title,
72-
titleTag: item.titleTag,
73-
titleTagStyle: item.titleTagStyle ?? .body,
74-
titleExtra: item.titleExtra,
75-
subtitle: item.subtitle,
76-
subtitleExtra: item.subtitleExtra,
77-
subtitleStyleExtra: item.subtitleExtraStyle ?? .calloutSecondary,
78-
infoAction: item.infoSheetType.map { type in { model.isPresentingInfoSheet = type } }
79-
)
67+
.explorerContext(explorerContext)
68+
case .info(let type):
69+
marketItemView(item, infoAction: { model.onSelectInfoSheet(type) })
70+
case .none:
71+
marketItemView(item)
8072
}
8173
}
8274
}
8375
}
76+
77+
private func marketItemView(_ item: MarketValueViewModel, infoAction: (() -> Void)? = nil) -> some View {
78+
ListItemView(
79+
title: item.title,
80+
titleTag: item.titleTag,
81+
titleTagStyle: item.titleTagStyle ?? .body,
82+
titleExtra: item.titleExtra,
83+
subtitle: item.subtitle,
84+
subtitleExtra: item.subtitleExtra,
85+
subtitleStyleExtra: item.subtitleExtraStyle ?? .calloutSecondary,
86+
infoAction: infoAction
87+
)
88+
}
8489
}

Features/MarketInsight/Sources/ViewModels/AssetDetailsInfoViewModel.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ struct AssetDetailsInfoViewModel {
5252
MarketValueViewModel(
5353
title: Localized.Asset.contract,
5454
subtitle: contractText,
55-
value: contract,
56-
url: contractUrl
55+
action: contract.flatMap { contract in
56+
contractExplorerLink.map {
57+
MarketValueViewModel.Action.explorer(
58+
ExplorerContextData(copyValue: .address(value: contract, chain: priceData.asset.chain), explorerLink: $0)
59+
)
60+
}
61+
} ?? .none
5762
)
5863
}
5964

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

68-
private var contractUrl: URL? {
69-
contract.flatMap { explorerService.tokenUrl(chain: priceData.asset.chain, address: $0)?.url }
73+
private var contractExplorerLink: BlockExplorerLink? {
74+
contract.flatMap { explorerService.tokenUrl(chain: priceData.asset.chain, address: $0) }
7075
}
7176
}

Features/MarketInsight/Sources/ViewModels/AssetMarketViewModel.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct AssetMarketViewModel {
4848
MarketValueViewModel(
4949
title: Localized.Info.FullyDilutedValuation.title,
5050
subtitle: formatCurrency(market.marketCapFdv),
51-
infoSheetType: .fullyDilutedValuation
51+
action: .info(.fullyDilutedValuation)
5252
)
5353
}
5454

@@ -58,23 +58,23 @@ struct AssetMarketViewModel {
5858
MarketValueViewModel(
5959
title: Localized.Asset.circulatingSupply,
6060
subtitle: formatSupply(market.circulatingSupply),
61-
infoSheetType: .circulatingSupply
61+
action: .info(.circulatingSupply)
6262
)
6363
}
6464

6565
var totalSupply: MarketValueViewModel {
6666
MarketValueViewModel(
6767
title: Localized.Asset.totalSupply,
6868
subtitle: formatSupply(market.totalSupply),
69-
infoSheetType: .totalSupply
69+
action: .info(.totalSupply)
7070
)
7171
}
7272

7373
var maxSupply: MarketValueViewModel {
7474
MarketValueViewModel(
7575
title: Localized.Info.MaxSupply.title,
7676
subtitle: market.maxSupply == 0 ? "\(assetSymbol)" : formatSupply(market.maxSupply),
77-
infoSheetType: .maxSupply
77+
action: .info(.maxSupply)
7878
)
7979
}
8080

Features/MarketInsight/Sources/ViewModels/ChartSceneViewModel.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,8 @@ extension ChartSceneViewModel {
105105
public func onSelectSetPriceAlerts() {
106106
onSetPriceAlert(assetModel.asset)
107107
}
108+
109+
func onSelectInfoSheet(_ type: InfoSheetType) {
110+
isPresentingInfoSheet = type
111+
}
108112
}

Features/MarketInsight/Sources/ViewModels/MarketValueViewModel.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,42 @@ import Foundation
44
import SwiftUI
55
import Style
66
import InfoSheet
7+
import PrimitivesComponents
78

89
struct MarketValueViewModel {
10+
enum Action {
11+
case none
12+
case explorer(ExplorerContextData)
13+
case info(InfoSheetType)
14+
}
15+
916
let title: String
1017
let titleExtra: String?
1118
let subtitle: String?
1219
let subtitleExtra: String?
1320
let subtitleExtraStyle: TextStyle?
14-
let value: String?
15-
let url: URL?
21+
let action: Action
1622
let titleTag: String?
1723
let titleTagStyle: TextStyle?
18-
let infoSheetType: InfoSheetType?
1924

2025
init(
2126
title: String,
2227
titleExtra: String? = .none,
2328
subtitle: String?,
2429
subtitleExtra: String? = .none,
2530
subtitleExtraStyle: TextStyle? = .none,
26-
value: String? = .none,
27-
url: URL? = .none,
31+
action: Action = .none,
2832
titleTag: String? = .none,
29-
titleTagStyle: TextStyle? = .none,
30-
infoSheetType: InfoSheetType? = .none
33+
titleTagStyle: TextStyle? = .none
3134
) {
3235
self.title = title
3336
self.titleExtra = titleExtra
3437
self.subtitle = subtitle
3538
self.subtitleExtra = subtitleExtra
3639
self.subtitleExtraStyle = subtitleExtraStyle
37-
self.value = value
38-
self.url = url
40+
self.action = action
3941
self.titleTag = titleTag
4042
self.titleTagStyle = titleTagStyle
41-
self.infoSheetType = infoSheetType
4243
}
4344
}
4445

Features/NFT/Sources/Scenes/CollectibleScene.swift

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ public struct CollectibleScene: View {
4646
}
4747
.alertSheet($model.isPresentingAlertMessage)
4848
.toast(message: $model.isPresentingToast)
49-
.safariSheet(url: $model.isPresentingTokenExplorerUrl)
5049
.sheet(isPresented: $model.isPresentingReportSheet) {
5150
ReportNavigationStack(
5251
model: ReportNftViewModel(
@@ -102,12 +101,10 @@ extension CollectibleScene {
102101
assetImage: model.networkAssetImage
103102
)
104103

105-
if let contractField = model.contractField {
106-
ListItemView(field: contractField)
107-
.contextMenu(model.contractContextMenu)
104+
if let contractRow = model.contractRow {
105+
infoRowView(contractRow)
108106
}
109-
ListItemView(field: model.tokenIdField)
110-
.contextMenu(model.tokenIdContextMenu)
107+
infoRowView(model.tokenIdRow)
111108
}
112109
}
113110

@@ -124,5 +121,16 @@ extension CollectibleScene {
124121
SocialLinksView(model: model.socialLinksViewModel)
125122
}
126123
}
127-
}
128124

125+
@ViewBuilder
126+
private func infoRowView(_ row: CollectibleInfoRow) -> some View {
127+
switch row.action {
128+
case .explorer(let explorerContext):
129+
ListItemView(field: row.field)
130+
.explorerContext(explorerContext)
131+
case .copy(let copyValue):
132+
ListItemView(field: row.field)
133+
.contextMenu(.copy(value: copyValue, onCopy: model.onSelectCopyValue))
134+
}
135+
}
136+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c). Gem Wallet. All rights reserved.
2+
3+
import Components
4+
import PrimitivesComponents
5+
6+
struct CollectibleInfoRow {
7+
enum Action {
8+
case copy(String)
9+
case explorer(ExplorerContextData)
10+
}
11+
12+
let field: ListItemField
13+
let action: Action
14+
}

Features/NFT/Sources/ViewModels/CollectibleViewModel.swift

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ public final class CollectibleViewModel {
2626

2727
var isPresentingAlertMessage: AlertMessage?
2828
var isPresentingToast: ToastMessage?
29-
var isPresentingTokenExplorerUrl: URL?
3029
var isPresentingSelectedAssetInput: Binding<SelectedAssetInput?>
3130
var isPresentingReportSheet = false
3231
var isPresentingInfoSheet: InfoSheetType?
@@ -86,27 +85,49 @@ public final class CollectibleViewModel {
8685
return ListItemField(title: Localized.Asset.contract, value: text)
8786
}
8887

89-
var contractContextMenu: [ContextMenuItemType] {
90-
[
91-
.copy(value: contractValue, onCopy: { [weak self] value in
92-
self?.isPresentingToast = .copied(value)
93-
}),
94-
contractExplorerUrl.map {
95-
.url(title: Localized.Transaction.viewOn($0.name), onOpen: onSelectViewContractInExplorer)
96-
}
97-
].compactMap { $0 }
88+
var contractExplorerLink: BlockExplorerLink? {
89+
explorerService.tokenUrl(chain: assetData.asset.chain, address: contractValue)
90+
}
91+
92+
var contractExplorerContext: ExplorerContextData? {
93+
contractExplorerLink.map {
94+
ExplorerContextData(copyValue: .address(value: contractValue, chain: assetData.asset.chain), explorerLink: $0)
95+
}
96+
}
97+
98+
var contractRow: CollectibleInfoRow? {
99+
contractField.map {
100+
CollectibleInfoRow(
101+
field: $0,
102+
action: contractExplorerContext.map { .explorer($0) } ?? .copy(contractValue)
103+
)
104+
}
98105
}
99106

100107
var tokenIdValue: String { assetData.asset.tokenId }
101108
var tokenIdField: ListItemField {
102109
let text = if assetData.asset.tokenId.count > 16 {
103-
assetData.asset.tokenId
110+
AddressFormatter(address: assetData.asset.tokenId, chain: assetData.asset.chain).value()
104111
} else {
105112
"#\(assetData.asset.tokenId)"
106113
}
107114
return ListItemField(title: Localized.Asset.tokenId, value: text)
108115
}
109116

117+
var tokenIdExplorerLink: BlockExplorerLink? {
118+
explorerService.nftUrl(
119+
chain: assetData.asset.chain,
120+
contractAddress: contractValue,
121+
tokenId: tokenIdValue
122+
)
123+
}
124+
125+
var tokenIdExplorerContext: ExplorerContextData? {
126+
tokenIdExplorerLink.map {
127+
ExplorerContextData(copyValue: .plain(tokenIdValue), explorerLink: $0)
128+
}
129+
}
130+
110131
var attributesTitle: String { Localized.Nft.properties }
111132
var attributes: [NFTAttribute] { assetData.asset.attributes }
112133

@@ -169,27 +190,25 @@ public final class CollectibleViewModel {
169190
SocialLinksViewModel(assetLinks: assetData.collection.links)
170191
}
171192

172-
var tokenExplorerUrl: BlockExplorerLink? {
173-
explorerService.tokenUrl(chain: assetData.asset.chain, address: assetData.asset.tokenId)
174-
}
175-
176-
var tokenIdContextMenu: [ContextMenuItemType] {
177-
let items: [ContextMenuItemType] = [
178-
.copy(value: tokenIdValue, onCopy: { [weak self] value in
179-
self?.isPresentingToast = .copied(value)
180-
}),
181-
tokenExplorerUrl.map {
182-
.url(title: Localized.Transaction.viewOn($0.name), onOpen: onSelectViewTokenInExplorer)
183-
}
184-
].compactMap { $0 }
185-
186-
return items
193+
var tokenIdRow: CollectibleInfoRow {
194+
CollectibleInfoRow(
195+
field: tokenIdField,
196+
action: tokenIdExplorerContext.map { .explorer($0) } ?? .copy(tokenIdValue)
197+
)
187198
}
188199
}
189200

190201
// MARK: - Business Logic
191202

192203
extension CollectibleViewModel {
204+
func onSelectCopyValue(_ value: CopyValue) {
205+
isPresentingToast = .copied(value.displayValue)
206+
}
207+
208+
func onSelectCopyValue(_ value: String) {
209+
isPresentingToast = .copied(value)
210+
}
211+
193212
func onSelectHeaderButton(type: HeaderButtonType) {
194213
guard let account = try? wallet.account(for: assetData.asset.chain) else {
195214
return
@@ -247,14 +266,6 @@ extension CollectibleViewModel {
247266
}
248267
}
249268

250-
func onSelectViewTokenInExplorer() {
251-
isPresentingTokenExplorerUrl = tokenExplorerUrl?.url
252-
}
253-
254-
func onSelectViewContractInExplorer() {
255-
isPresentingTokenExplorerUrl = contractExplorerUrl?.url
256-
}
257-
258269
func onSelectReport() {
259270
isPresentingReportSheet = true
260271
}
@@ -274,9 +285,6 @@ extension CollectibleViewModel {
274285
extension CollectibleViewModel {
275286
private static let enabledChainTypes: Set<ChainType> = [.ethereum]
276287
private var contractValue: String { assetData.collection.contractAddress }
277-
private var contractExplorerUrl: BlockExplorerLink? {
278-
explorerService.tokenUrl(chain: assetData.asset.chain, address: contractValue)
279-
}
280288

281289
private func openSettings() {
282290
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return }

0 commit comments

Comments
 (0)