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 @@ -620,6 +620,10 @@
5AEDE0012F9B000000000004 /* EvonodeWithdrawalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEDE0012F9B000000000001 /* EvonodeWithdrawalViewModel.swift */; };
5AEDE0012F9B000000000005 /* EvonodeWithdrawalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEDE0012F9B000000000002 /* EvonodeWithdrawalScreen.swift */; };
5AEDE0012F9B000000000006 /* EvonodeWithdrawalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AEDE0012F9B000000000002 /* EvonodeWithdrawalScreen.swift */; };
5AE5700E2F9C000000000003 /* EvonodeStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */; };
5AE5700E2F9C000000000004 /* EvonodeStatusViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */; };
5AE5700E2F9C000000000005 /* EvonodeStatusScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000002 /* EvonodeStatusScreen.swift */; };
5AE5700E2F9C000000000006 /* EvonodeStatusScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AE5700E2F9C000000000002 /* EvonodeStatusScreen.swift */; };
51BA2EA02F0B2E0100A1B201 /* SwapPendingGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BA2E9F2F0B2E0100A1B201 /* SwapPendingGate.swift */; };
51BA2EA12F0B2E0100A1B201 /* SwapPendingGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BA2E9F2F0B2E0100A1B201 /* SwapPendingGate.swift */; };
51F0D10130BF97A900B27E11 /* ExclamationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F0D10030BF97A900B27E11 /* ExclamationIcon.swift */; };
Expand Down Expand Up @@ -2815,6 +2819,8 @@
51AA00202F970020005A0020 /* MasternodesScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MasternodesScreen.swift; sourceTree = "<group>"; };
5AEDE0012F9B000000000001 /* EvonodeWithdrawalViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeWithdrawalViewModel.swift; sourceTree = "<group>"; };
5AEDE0012F9B000000000002 /* EvonodeWithdrawalScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeWithdrawalScreen.swift; sourceTree = "<group>"; };
5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeStatusViewModel.swift; sourceTree = "<group>"; };
5AE5700E2F9C000000000002 /* EvonodeStatusScreen.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvonodeStatusScreen.swift; sourceTree = "<group>"; };
51BA2E9F2F0B2E0100A1B201 /* SwapPendingGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapPendingGate.swift; sourceTree = "<group>"; };
51DFA7F72FA8A9DF001EDB3A /* DashStepper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashStepper.swift; sourceTree = "<group>"; };
51F0D10030BF97A900B27E11 /* ExclamationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExclamationIcon.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3783,6 +3789,15 @@
path = "Masternode Withdrawal";
sourceTree = "<group>";
};
5AE5700E2F9C000000000007 /* Evonode Status */ = {
isa = PBXGroup;
children = (
5AE5700E2F9C000000000001 /* EvonodeStatusViewModel.swift */,
5AE5700E2F9C000000000002 /* EvonodeStatusScreen.swift */,
);
path = "Evonode Status";
sourceTree = "<group>";
};
0529717581A09721B3115957 /* InternalTransfer */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4826,6 +4841,7 @@
FB3FF0B0222641210059A9A5 /* Masternode Keys */,
51AA00202F970020005A0020 /* MasternodesScreen.swift */,
5AEDE0012F9B000000000007 /* Masternode Withdrawal */,
5AE5700E2F9C000000000007 /* Evonode Status */,
7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */,
RC00C0032FA0000000000001 /* CSVExportSheet.swift */,
752F81A82E323FD000ADA76D /* ToolsMenuViewModel.swift */,
Expand Down Expand Up @@ -10208,6 +10224,8 @@
51AA00222F970022005A0022 /* MasternodesScreen.swift in Sources */,
5AEDE0012F9B000000000004 /* EvonodeWithdrawalViewModel.swift in Sources */,
5AEDE0012F9B000000000006 /* EvonodeWithdrawalScreen.swift in Sources */,
5AE5700E2F9C000000000004 /* EvonodeStatusViewModel.swift in Sources */,
5AE5700E2F9C000000000006 /* EvonodeStatusScreen.swift in Sources */,
51AA00022F970002005A0002 /* SyncInfoMenuScreen.swift in Sources */,
51AA00052F970005005A0005 /* SyncInfoMenuViewModel.swift in Sources */,
51AA00082F970008005A0008 /* DashPaySyncInfoScreen.swift in Sources */,
Expand Down Expand Up @@ -11222,6 +11240,8 @@
51AA00212F970021005A0021 /* MasternodesScreen.swift in Sources */,
5AEDE0012F9B000000000003 /* EvonodeWithdrawalViewModel.swift in Sources */,
5AEDE0012F9B000000000005 /* EvonodeWithdrawalScreen.swift in Sources */,
5AE5700E2F9C000000000003 /* EvonodeStatusViewModel.swift in Sources */,
5AE5700E2F9C000000000005 /* EvonodeStatusScreen.swift in Sources */,
51AA00032F970003005A0003 /* SyncInfoMenuScreen.swift in Sources */,
51AA00062F970006005A0006 /* SyncInfoMenuViewModel.swift in Sources */,
51AA00092F970009005A0009 /* DashPaySyncInfoScreen.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//
// EvonodeStatusScreen.swift
// DashWallet
//
// 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 SwiftDashSDK
import SwiftUI

// MARK: - EvonodeStatusScreen

/// Everything one evonode answered to a DAPI `getStatus` request, grouped
/// as the node groups it. Pushed from the evonode detail screen's "Request
/// status" button; the request itself is sent when this screen appears and
/// again on Refresh — never on its own.
struct EvonodeStatusScreen: View {
@StateObject private var viewModel: EvonodeStatusViewModel

init(masternode: PlatformMasternode) {
_viewModel = StateObject(wrappedValue: EvonodeStatusViewModel(masternode: masternode))
}

var body: some View {
content
.navigationTitle(NSLocalizedString("Node status", comment: "Evonode status"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
Task { await viewModel.request() }
} label: {
Image(systemName: "arrow.clockwise")
}
.disabled(viewModel.isLoading)
.accessibilityLabel(NSLocalizedString("Refresh", comment: ""))
}
}
.task {
// The user reached this screen by tapping "Request status" —
// that tap is the request. Re-appearing (back from a pushed
// screen) keeps the last answer; Refresh asks again.
if viewModel.phase == .idle {
await viewModel.request()
}
}
}

// MARK: Bodies

/// A refresh keeps the previous answer on screen (with a small spinner)
/// instead of blanking it; only the first request shows the full loader.
@ViewBuilder
private var content: some View {
switch viewModel.phase {
case .idle:
loadingBody
case .loading:
if viewModel.sections.isEmpty {
loadingBody
} else {
report
}
case .failed(let title, let detail):
failedBody(title: title, detail: detail)
case .loaded:
report
}
}

private var loadingBody: some View {
VStack(spacing: 12) {
// Explicitly SwiftUI's — the app declares its own `ProgressView`
// UIKit class that shadows it here.
SwiftUI.ProgressView()
Text(NSLocalizedString("Asking the evonode…", comment: "Evonode status"))
.font(.subheadline)
.foregroundColor(Color.dash.secondaryText)
if let address = viewModel.address {
Text(address)
.font(.system(.caption, design: .monospaced))
.foregroundColor(Color.dash.tertiaryText)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

private func failedBody(title: String, detail: String?) -> some View {
VStack(spacing: 12) {
Image(systemName: "antenna.radiowaves.left.and.right.slash")
.font(.system(size: 40))
.foregroundColor(Color.dash.tertiaryText)

Text(title)
.font(.headline)
.multilineTextAlignment(.center)

if let address = viewModel.address {
Text(address)
.font(.system(.caption, design: .monospaced))
.foregroundColor(Color.dash.tertiaryText)
}

if let detail, !detail.isEmpty {
Text(detail)
.font(.caption)
.foregroundColor(Color.dash.secondaryText)
.multilineTextAlignment(.center)
.textSelection(.enabled)
}

if viewModel.address != nil {
DashButton(
text: NSLocalizedString("Try again", comment: ""),
style: .filled,
stretch: false,
action: { Task { await viewModel.request() } })
.padding(.top, 8)
}
}
.padding(.horizontal, 24)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

private var report: some View {
List {
ForEach(viewModel.sections) { section in
Section {
ForEach(section.rows) { row in
switch row.style {
case .value:
MasternodeDetailRow(label: row.label, value: row.value)
case .copyable:
MasternodeCopyRow(label: row.label, value: row.value)
}
}
} header: {
Text(section.title)
} footer: {
if let footer = section.footer {
Text(footer)
}
}
}
}
.overlay(alignment: .top) {
if viewModel.isLoading {
SwiftUI.ProgressView()
.padding(8)
.background(.thinMaterial, in: Capsule())
.padding(.top, 8)
}
}
}
}
Loading
Loading