Skip to content
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
20 changes: 20 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,10 @@
756557B62CE84FFA0060348D /* FeeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756557B42CE84FF70060348D /* FeeInfo.swift */; };
7566F4832BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */; };
7566F4842BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */; };
5AGOV0002F9A0000000000S2 /* GovernanceMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */; };
5AGOV0002F9A0000000000S3 /* GovernanceMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */; };
5AGOV0002F9A0000000000V2 /* GovernanceMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */; };
5AGOV0002F9A0000000000V3 /* GovernanceMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */; };
7566F48A2BB6CAF2005238D2 /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4892BB6CAF2005238D2 /* MenuItem.swift */; };
7566F48B2BB6CAF2005238D2 /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4892BB6CAF2005238D2 /* MenuItem.swift */; };
7569D6832DE6DA6800768BFF /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7569D6822DE6DA6800768BFF /* ExploreViewController.swift */; };
Expand Down Expand Up @@ -2897,6 +2901,8 @@
755E6DFD2A99E7A000A42870 /* DWInvitationSetupState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWInvitationSetupState.h; sourceTree = "<group>"; };
756557B42CE84FF70060348D /* FeeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeInfo.swift; sourceTree = "<group>"; };
7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolsMenuScreen.swift; sourceTree = "<group>"; };
5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovernanceMenuScreen.swift; sourceTree = "<group>"; };
5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovernanceMenuViewModel.swift; sourceTree = "<group>"; };
7566F4892BB6CAF2005238D2 /* MenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = "<group>"; };
7569D6822DE6DA6800768BFF /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
7569D6852DE6DB1B00768BFF /* ExploreHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreHeaderView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4722,6 +4728,7 @@
2A7A7BD32348CB4F00451078 /* Settings */,
51AA00102F970010005A0010 /* SyncInfo */,
2A7A7BDA2348DBE700451078 /* Tools */,
5AGOV0002F9A0000000000G0 /* Governance */,
755049AB2C846576008FA7EB /* MenuItemModel.swift */,
);
path = Menu;
Expand Down Expand Up @@ -4760,6 +4767,15 @@
path = Settings;
sourceTree = "<group>";
};
5AGOV0002F9A0000000000G0 /* Governance */ = {
isa = PBXGroup;
children = (
5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */,
5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */,
);
path = Governance;
sourceTree = "<group>";
};
2A7A7BDA2348DBE700451078 /* Tools */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -10039,6 +10055,8 @@
47CF46A1296540EF0067B6EE /* AccountService.swift in Sources */,
47C6E6E7291A90B3003FEDF2 /* ListHandlerView.swift in Sources */,
7566F4832BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */,
5AGOV0002F9A0000000000S2 /* GovernanceMenuScreen.swift in Sources */,
5AGOV0002F9A0000000000V2 /* GovernanceMenuViewModel.swift in Sources */,
RC00C0012FA0000000000001 /* CSVExportSheet.swift in Sources */,
754D020E2D1558EB005CA466 /* GroupedTransactions.swift in Sources */,
11BD738128E7356100A34022 /* CrowdNode.swift in Sources */,
Expand Down Expand Up @@ -10664,6 +10682,8 @@
C9D2C7AB2A320AA000D15901 /* DWBaseFormTableViewCell.m in Sources */,
C9D2C7AC2A320AA000D15901 /* AccountCell.swift in Sources */,
7566F4842BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */,
5AGOV0002F9A0000000000S3 /* GovernanceMenuScreen.swift in Sources */,
5AGOV0002F9A0000000000V3 /* GovernanceMenuViewModel.swift in Sources */,
RC00C0022FA0000000000001 /* CSVExportSheet.swift in Sources */,
C943B33A2A408CED00AF23C5 /* DWUploadAvatarModel.m in Sources */,
C943B3392A408CED00AF23C5 /* DWUploadAvatarViewController.m in Sources */,
Expand Down
128 changes: 128 additions & 0 deletions DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// Copyright © 2026 Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import DashUIKit
import SwiftUI
import UIKit

/// Governance submenu: the wallet's masternode-facing surfaces, reached from
/// the main menu's "Governance" row. Groups Masternodes (moved here from
/// Tools) with username Voting (moved here from the main menu).
struct GovernanceMenuScreen: View {
private let vc: UINavigationController

@StateObject private var viewModel = GovernanceMenuViewModel()

init(vc: UINavigationController) {
self.vc = vc
}

var body: some View {
VStack(alignment: .leading, spacing: 0) {
NavBarBack {
vc.popViewController(animated: true)
}

TopIntro(title: NSLocalizedString("Governance", comment: "Governance"))
.padding(.leading, 20)
.padding(.trailing, 60)
.padding(.top, 10)
.padding(.bottom, 20)

VStack(spacing: 2) {
ForEach(viewModel.items) { item in
MenuItem(
title: item.title,
subtitle: item.subtitle,
details: item.details,
icon: item.icon,
showInfo: item.showInfo,
showChevron: false,
isToggled: item.isToggled,
action: item.action
)
.frame(minHeight: 56)
}
}
.padding(6)
.background(Color.dash.secondaryBackground)
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
.shadow(color: Color.dash.shadow, radius: 20, x: 0, y: 5)
.padding(.horizontal, 20)

Spacer()
}
.background(Color.dash.primaryBackground)
.navigationBarHidden(true)
.onReceive(viewModel.$navigationDestination) { destination in
handleNavigation(destination)
}
}

private func handleNavigation(_ destination: GovernanceMenuNavigationDestination?) {
switch destination {
case .masternodes:
showMasternodes()
#if DASHPAY
case .voting:
showVoting()
#endif
case .none:
break
}

if destination != nil {
viewModel.resetNavigation()
}
}

/// Same hosting wrapper Tools used when this screen lived there: the
/// SwiftUI list needs its own `NavigationStack` for row drill-downs, so
/// the back button is wired explicitly to pop the UIKit stack.
private func showMasternodes() {
let navController = vc
let popRoot: () -> Void = { [weak navController] in
_ = navController?.popViewController(animated: true)
}

let hosting = UIHostingController(
rootView: AnyView(
NavigationStack {
MasternodesScreen()
.navigationTitle(NSLocalizedString("Masternodes", comment: "Masternodes"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: popRoot) {
Image(systemName: "chevron.left")
}
}
}
}
)
)
hosting.hidesBottomBarWhenPushed = true
vc.pushViewController(hosting, animated: true)
}

#if DASHPAY
private func showVoting() {
let controller = UsernameVotingViewController.controller()
controller.hidesBottomBarWhenPushed = true
vc.pushViewController(controller, animated: true)
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Copyright © 2026 Dash Core Group. All rights reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import Foundation

enum GovernanceMenuNavigationDestination {
case masternodes
#if DASHPAY
case voting
#endif
}

@MainActor
final class GovernanceMenuViewModel: ObservableObject {
@Published var items: [MenuItemModel] = []
Comment thread
coderabbitai[bot] marked this conversation as resolved.
@Published var navigationDestination: GovernanceMenuNavigationDestination?

init() {
setupMenuItems()
}

private func setupMenuItems() {
var allItems: [MenuItemModel] = []

// Masternodes — not DashPay-gated: masternode ownership is a Core
// concern and this screen is reachable in every build configuration
// (it previously lived under Tools with no gate).
allItems.append(MenuItemModel(
title: NSLocalizedString("Masternodes", comment: "Masternodes"),
subtitle: NSLocalizedString(
"Masternodes and evonodes using this wallet's keys", comment: "Masternodes"),
icon: .system("server.rack"),
action: { [weak self] in
self?.navigationDestination = .masternodes
}
))

#if DASHPAY
// Username voting, behind the same preference that gated it on the
// main menu (Settings → "Username voting").
if VotingPrefs.shared.votingEnabled {
allItems.append(MenuItemModel(
title: NSLocalizedString("Voting", comment: ""),
subtitle: NSLocalizedString(
"Vote on contested usernames", comment: "Voting"),
icon: .custom("menu_voting", maxHeight: 30),
action: { [weak self] in
self?.navigationDestination = .voting
}
))
}
#endif

items = allItems
}

func resetNavigation() {
navigationDestination = nil
}
}
14 changes: 10 additions & 4 deletions DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ struct MainMenuScreen: View {
@ObservedObject private var viewModel: MainMenuViewModel
@State private var openSettings: Bool = false
@State private var showTools: Bool = false
@State private var showGovernance: Bool = false
@State private var showSyncInfo: Bool = false
@State private var showWallets: Bool = false
@State private var showIdentities: Bool = false
Expand Down Expand Up @@ -337,6 +338,13 @@ struct MainMenuScreen: View {
) {
EmptyView()
}

NavigationLink(
destination: GovernanceMenuScreen(vc: vc),
isActive: $showGovernance
) {
EmptyView()
}

NavigationLink(
destination: SyncInfoMenuScreen(vc: vc),
Expand Down Expand Up @@ -451,10 +459,8 @@ struct MainMenuScreen: View {
showTools = true
case .support:
onContactSupport()
#if DASHPAY
case .voting:
showVoting()
#endif
case .governance:
showGovernance = true
case .none:
return
}
Expand Down
26 changes: 11 additions & 15 deletions DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ enum MainMenuNavigationDestination {
case settings
case tools
case support
#if DASHPAY
case voting
#endif
case governance
}

protocol MainMenuViewModelDelegate: AnyObject {
Expand Down Expand Up @@ -166,18 +164,16 @@ class MainMenuViewModel: ObservableObject {
}
))

#if DASHPAY
// Voting
if VotingPrefs.shared.votingEnabled {
allItems.append(MenuItemModel(
title: NSLocalizedString("Voting", comment: ""),
icon: .custom("menu_voting", maxHeight: 30),
action: { [weak self] in
self?.navigationDestination = .voting
}
))
}
#endif
// Governance — Masternodes plus (on DashPay builds, when enabled)
// username Voting. Not DashPay-gated: Masternodes is a Core-side
// surface that every build configuration can reach.
allItems.append(MenuItemModel(
title: NSLocalizedString("Governance", comment: "Governance"),
icon: .custom("menu_voting", maxHeight: 30),
action: { [weak self] in
self?.navigationDestination = .governance
}
))

self.items = allItems
}
Expand Down
Loading
Loading