From 6a73e580e90a9b2123f52f985d8c1502909ef205 Mon Sep 17 00:00:00 2001 From: Roman <51091564+jeanpierreroma@users.noreply.github.com> Date: Mon, 3 Aug 2026 19:58:38 +0300 Subject: [PATCH] feat(dashpay): persistent username row in the home header nav bar (SB-11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The DashPay profile entry point lived only on the UIKit navigation bar's avatar, which #813 made scroll-gated: hidden at the top of the feed and revealed past `kTopBarShowThreshold`. On a feed shorter than the viewport that threshold can never be crossed, so the avatar — and with it the only way into "my profile" — was unreachable. Move the bar into the balance header, which is always on screen at the top of Home: `DashUIKit.NavigationBar` with the username row leading, the Dash wordmark central and the notifications bell trailing. `HomeUsernameRow` reuses `ContactAvatarView` and the existing `profileAction()` target, so there is one source of truth for opening the profile rather than a second tap path. `HomeView` measures scrollable-content height against the ScrollView's own viewport (`HomeContentHeightKey` / `HomeViewportHeightKey`) and keeps the old UIKit bar shown when the feed is too short to scroll the threshold, so the existing reveal gesture cannot strand it either. A zero viewport height means the first layout pass has not reported yet — it waits for a real measurement instead of flashing the bar on a feed that is long enough. The bell's unread state and the username/avatar are read from the same app-owned identity and contacts snapshots the UIKit bar uses, refreshed on `DWDashPayRegistrationStatusUpdated` and the contacts snapshot notification so a new username or incoming request lands without leaving Home. All of it is `#if DASHPAY`; the non-DashPay build compiles the bar out. Not verified by a build: the SwiftDashSDK package in ../platform is on a feature branch and does not compile, so the app target was never reached. Co-Authored-By: Claude Opus 5 --- DashWallet.xcodeproj/project.pbxproj | 6 + .../Contacts/SwiftUI/ContactAvatarView.swift | 30 ++++ .../Sources/UI/Home/HomeViewController.swift | 4 + .../Home Balance View/HomeBalanceView.swift | 72 ++++++++ .../UI/Home/Views/HomeUsernameRow.swift | 62 +++++++ .../Sources/UI/Home/Views/HomeView.swift | 156 +++++++++++++++++- 6 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 DashWallet/Sources/UI/Home/Views/HomeUsernameRow.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index cd62e7453..69254a852 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -487,6 +487,7 @@ 47B30D7C29100ABA0080C326 /* UIStackView+DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B30D7B29100ABA0080C326 /* UIStackView+DashWallet.swift */; }; 47B30D7E29102F6E0080C326 /* SendAmountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B30D7D29102F6E0080C326 /* SendAmountModel.swift */; }; 47B30D80291123D30080C326 /* SendAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B30D7F291123D30080C326 /* SendAmountViewController.swift */; }; + 47B81672DEBF6B7B565F0AC8 /* HomeUsernameRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E55DD28C4EBE049216291A2 /* HomeUsernameRow.swift */; }; 47C661AB28F9707A00028A8D /* NumberKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661AA28F9707A00028A8D /* NumberKeyboard.swift */; }; 47C661AD28F972BD00028A8D /* NumberKeyboardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661AC28F972BD00028A8D /* NumberKeyboardButton.swift */; }; 47C661AF28FDAA3400028A8D /* BaseAmountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C661AE28FDAA3300028A8D /* BaseAmountViewController.swift */; }; @@ -983,6 +984,7 @@ A15ED0B9E7AAF29C694E4E17 /* DWIdentityAuthorizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33FBA94DA84A9EE303FF48B4 /* DWIdentityAuthorizer.swift */; }; A17EED0126C7000000000001 /* WalletWipeSerialExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A17EED0126C7000000000002 /* WalletWipeSerialExecutorTests.swift */; }; A361573C34CF9107E6B8A221 /* SwiftDashSDKWalletCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B697D56A525CBF142F4401FE /* SwiftDashSDKWalletCreator.swift */; }; + A365C2F95472285830E6CDEB /* HomeUsernameRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E55DD28C4EBE049216291A2 /* HomeUsernameRow.swift */; }; A3ED42ED80F32AA5CA530A40 /* DamerauLevenshtein.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE60A95629A2883710A75C /* DamerauLevenshtein.swift */; }; A5C8B5B00075FB8E84009327 /* SendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E6FAA6524ACE5F315B57AC3 /* SendViewModel.swift */; }; A6AAB271A1ADA69C272DA1D4 /* PinPromptPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D385FC174E5B887CF3398CCD /* PinPromptPresenter.swift */; }; @@ -2747,6 +2749,7 @@ 487C4C2E6AC61005536FF85E /* NetworkUnavailableStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkUnavailableStateView.swift; sourceTree = ""; }; 48D8BC6623C81DD3F008A668 /* DWPreviewSeedPhraseModel+Mnemonic.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "DWPreviewSeedPhraseModel+Mnemonic.swift"; sourceTree = ""; }; 4C4833BE986D59F69A555C5F /* PaymentProtocolMessages.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PaymentProtocolMessages.swift; sourceTree = ""; }; + 4E55DD28C4EBE049216291A2 /* HomeUsernameRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeUsernameRow.swift; sourceTree = ""; }; 4E9C4B39B015CD9D8AD4741D /* SwiftDashSDKWalletRuntime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwiftDashSDKWalletRuntime.swift; sourceTree = ""; }; 514AEA122FB324FA00F9B54C /* SwapConvertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConvertView.swift; sourceTree = ""; }; 514AEA4A2FB33B4300F9B54C /* SwapConvertHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConvertHostingController.swift; sourceTree = ""; }; @@ -4330,6 +4333,7 @@ 5A1EC0FE2E29A10000000001 /* ShieldedActivityHistory.swift */, 5A1EC0FE2E29A20000000014 /* PlatformAddressHistory.swift */, 75D657662DF579F300ACE570 /* TransactionFilterDialog.swift */, + 4E55DD28C4EBE049216291A2 /* HomeUsernameRow.swift */, ); path = Views; sourceTree = ""; @@ -10190,6 +10194,7 @@ 3B9C0EAAC6DE69DBF8271920 /* BIP70PaymentOutputFactory.swift in Sources */, F498EAE11BC0B3684DBABCC7 /* DiagnosticLogExporter.swift in Sources */, 197F19679BBAABA2E99503B8 /* DWAvatarUploadClient.swift in Sources */, + 47B81672DEBF6B7B565F0AC8 /* HomeUsernameRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -11190,6 +11195,7 @@ FCDF0FF6DC442DBE3C9713FD /* DiagnosticLogExporter.swift in Sources */, 4720E26BFE56A4559C602ED5 /* ShieldedIdentityFundingReadiness.swift in Sources */, D15607B6987D27ACDC844F9E /* DWAvatarUploadClient.swift in Sources */, + A365C2F95472285830E6CDEB /* HomeUsernameRow.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DashWallet/Sources/UI/DashPay/Contacts/SwiftUI/ContactAvatarView.swift b/DashWallet/Sources/UI/DashPay/Contacts/SwiftUI/ContactAvatarView.swift index 612c8ff20..f9644a170 100644 --- a/DashWallet/Sources/UI/DashPay/Contacts/SwiftUI/ContactAvatarView.swift +++ b/DashWallet/Sources/UI/DashPay/Contacts/SwiftUI/ContactAvatarView.swift @@ -168,3 +168,33 @@ struct ContactsCard: View { .fill(Color.dash.secondaryBackground)) } } + +#if DEBUG + +// No `avatarURL` here — the placeholder path is the deterministic, +// offline-renderable one; the letter and its HSV color both derive from +// `title`, so each initial gets its own hue. +#Preview("Placeholder initials") { + HStack(spacing: 12) { + ForEach(["Epsilon2", "Delta", "b-user", "Zoe", "7", "мій"], id: \.self) { title in + ContactAvatarView(title: title, avatarURL: nil, identitySeed: Data()) + } + } + .padding() +} + +#Preview("Sizes") { + HStack(alignment: .center, spacing: 12) { + ForEach([24.0, 36.0, 48.0, 64.0], id: \.self) { size in + ContactAvatarView(title: "Epsilon2", avatarURL: nil, identitySeed: Data(), size: size) + } + } + .padding() +} + +#Preview("Empty title (fallback)") { + ContactAvatarView(title: "", avatarURL: nil, identitySeed: Data(), size: 48) + .padding() +} + +#endif diff --git a/DashWallet/Sources/UI/Home/HomeViewController.swift b/DashWallet/Sources/UI/Home/HomeViewController.swift index 9bfb12a2b..7acd0b330 100644 --- a/DashWallet/Sources/UI/Home/HomeViewController.swift +++ b/DashWallet/Sources/UI/Home/HomeViewController.swift @@ -638,6 +638,10 @@ extension HomeViewController: HomeViewDelegate { func homeViewEditProfile() { profileAction() } + + func homeViewShowNotifications() { + notificationAction() + } #endif } diff --git a/DashWallet/Sources/UI/Home/Views/Home Balance View/HomeBalanceView.swift b/DashWallet/Sources/UI/Home/Views/Home Balance View/HomeBalanceView.swift index d84033569..28791a532 100644 --- a/DashWallet/Sources/UI/Home/Views/Home Balance View/HomeBalanceView.swift +++ b/DashWallet/Sources/UI/Home/Views/Home Balance View/HomeBalanceView.swift @@ -45,12 +45,36 @@ struct HomeBalanceView: View { /// what-is-this-balance info sheet for that balance. var onInfo: (ChainNetwork) -> Void = { _ in } + // Header nav-bar (SB-11) inputs, threaded in by HomeView from the same + // app-owned identity snapshot the UIKit nav-bar avatar reads. A nil + // `username` hides the leading row (no registered identity). These are + // plain values so the view stays dumb; only the DASHPAY nav bar below + // reads them. + var username: String? = nil + var avatarURL: String? = nil + var identitySeed: Data = Data() + var hasUnreadNotifications: Bool = false + var onProfileTap: () -> Void = {} + var onNotificationsTap: () -> Void = {} + private var platformDuffs: UInt64 { platformSync.platformBalance / 1_000 } private var shieldedDuffs: UInt64 { platformSync.shieldedBalance / 1_000 } private var totalDuffs: UInt64 { viewModel.value + platformDuffs + shieldedDuffs } var body: some View { VStack(spacing: 0) { + // Home nav bar (avatar/username · Dash logo · notifications). + // DashPay-only: the leading row and the bell are identity + // features, so the whole bar is compiled out of the non-DashPay + // build. + #if DASHPAY + DashUIKit.NavigationBar( + leading: { navLeading }, + central: { dashLogo }, + trailing: { notificationButton } + ) + #endif + if viewModel.isTestnet { testnetBadge } @@ -127,6 +151,54 @@ struct HomeBalanceView: View { } } + #if DASHPAY + /// Leading slot: the persistent username entry point. Empty (the nav bar + /// keeps its layout) until an identity with a username is registered. + @ViewBuilder + private var navLeading: some View { + if let username { + HomeUsernameRow( + username: username, + avatarURL: avatarURL, + identitySeed: identitySeed, + onTap: onProfileTap) + } + } + + /// Central slot: the full Dash wordmark. `logo` is the blue-on-transparent + /// wordmark asset; rendered as a template so it reads white on the blue + /// header, per the design-system nav bar. Height 22 matches the spec. + private var dashLogo: some View { + Image("logo") + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(height: 22) + .foregroundColor(Color.dash.whiteText) + .accessibilityLabel(Text(verbatim: "Dash")) + } + + /// Trailing slot: bell in a translucent circle (spec: 34pt circle, + /// white/alpha-20 fill). Reuses the app's white bell assets; + /// `icon_bell_active` carries the unread indicator, so it renders as-is + /// while the plain bell is tinted white. + private var notificationButton: some View { + Button(action: onNotificationsTap) { + Image(hasUnreadNotifications ? "icon_bell_active" : "icon_bell") + .renderingMode(hasUnreadNotifications ? .original : .template) + .resizable() + .scaledToFit() + .frame(width: 15, height: 18) + .foregroundColor(Color.dash.whiteText) + .frame(width: 34, height: 34) + .background(Circle().fill(Color.dash.white.opacity(0.2))) + .contentShape(Circle()) + } + .buttonStyle(.plain) + .accessibilityLabel(Text(NSLocalizedString("Notifications", comment: "Home nav bar"))) + } + #endif + private var breakdownCard: some View { VStack(spacing: 0) { balanceRow( diff --git a/DashWallet/Sources/UI/Home/Views/HomeUsernameRow.swift b/DashWallet/Sources/UI/Home/Views/HomeUsernameRow.swift new file mode 100644 index 000000000..c7eea7afa --- /dev/null +++ b/DashWallet/Sources/UI/Home/Views/HomeUsernameRow.swift @@ -0,0 +1,62 @@ +// +// HomeUsernameRow.swift +// DashWallet +// +// Persistent Home entry point for a registered DashPay username (SB-11). +// The nav-bar avatar (HomeViewController) is scroll-gated — hidden at the +// top of the feed and revealed only past `kTopBarShowThreshold`, so on a +// short feed it can be unreachable. This row sits inside the balance +// header, which is always on screen at the top of Home, and reuses the +// same tap target (`profileAction()` → `SDKIdentityProfileSheet`) so there +// is a single source of truth for "open my profile". +// + +import SwiftUI +import DashUIKit + +#if DASHPAY +struct HomeUsernameRow: View { + let username: String + let avatarURL: String? + let identitySeed: Data + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + HStack(spacing: 8) { + ContactAvatarView(title: username, avatarURL: avatarURL, identitySeed: identitySeed, size: 36) + } + } + .buttonStyle(.plain) + .accessibilityLabel(Text(String(format: NSLocalizedString("My profile, %@", comment: "Home — persistent username row accessibility label"), username))) + } +} + +#if DEBUG + +// Previewed on the balance header's color, where the row actually sits, so +// the white text/chevron contrast is exercised. The no-avatar case is the +// common one (registered username, `hasProfile=false` → letter placeholder). +#Preview("No avatar (placeholder)") { + HomeUsernameRow( + username: "Epsilon2", + avatarURL: nil, + identitySeed: Data([0x0e, 0x8f, 0xdf, 0x06]), + onTap: {}) + .padding(24) + .background(Color.navigationBarColor) +} + +#Preview("Long username (truncation)") { + HomeUsernameRow( + username: "a-very-long-dashpay-username", + avatarURL: nil, + identitySeed: Data([0x89, 0xfd, 0x6d, 0xdb]), + onTap: {}) + .frame(width: 200) + .padding(24) + .background(Color.navigationBarColor) +} + +#endif +#endif diff --git a/DashWallet/Sources/UI/Home/Views/HomeView.swift b/DashWallet/Sources/UI/Home/Views/HomeView.swift index ea52eb5b5..23ebbf58f 100644 --- a/DashWallet/Sources/UI/Home/Views/HomeView.swift +++ b/DashWallet/Sources/UI/Home/Views/HomeView.swift @@ -40,6 +40,8 @@ protocol HomeViewDelegate: AnyObject { func homeViewRequestUsername() func homeViewClaimInvitation() func homeViewEditProfile() + /// Opens the notifications list (header nav-bar bell). + func homeViewShowNotifications() #endif } @@ -199,6 +201,25 @@ private struct HomeScrollOffsetKey: PreferenceKey { } } +/// Total scrollable-content height, measured off the same sentinel as +/// `HomeScrollOffsetKey` — feeds the short-feed reveal fallback. +private struct HomeContentHeightKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +/// The `ScrollView`'s own viewport height — compared against +/// `HomeContentHeightKey` to detect a feed too short to cross +/// `kTopBarShowThreshold` by scrolling. +private struct HomeViewportHeightKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + struct HomeViewContent: View { @State private var selectedTxDataItem: TransactionListDataItem? = nil @State private var showFilterDialog: Bool = false @@ -210,6 +231,18 @@ struct HomeViewContent: View { /// Balance whose explainer sheet is up (tap on a breakdown row's body). @State private var balanceInfoNetwork: ChainNetwork? = nil + #if DASHPAY + /// Persistent username-row state (SB-11) — read from the same + /// app-owned identity snapshot the nav-bar avatar uses. Nil username + /// hides the row (no identity registered). + @State private var currentUsername: String? = nil + @State private var currentAvatarURL: String? = nil + @State private var currentIdentitySeed: Data = Data() + /// Drives the header bell's unread state. Read from the same contacts + /// bridge that feeds the UIKit nav-bar bell, refreshed on the contacts + /// snapshot-change notification so an incoming request lights it live. + @State private var currentHasNotifications: Bool = false + #endif @ObservedObject var viewModel: HomeViewModel @ObservedObject private var balanceModel = BalanceModel() @@ -227,6 +260,12 @@ struct HomeViewContent: View { private let topOverscrollSize: CGFloat = 1000 // Fixed value for top overscroll area @State private var isTopBarShown = false + /// Measured scrollable-content height vs. the ScrollView's own viewport + /// height — a short feed can never scroll `kTopBarShowThreshold` pt, so + /// without this the nav-bar avatar/username row would be permanently + /// stranded behind an unreachable reveal gesture. + @State private var scrollContentHeight: CGFloat = 0 + @State private var scrollViewportHeight: CGFloat = 0 var body: some View { ZStack { @@ -236,6 +275,10 @@ struct HomeViewContent: View { .padding(EdgeInsets(top: -topOverscrollSize, leading: 0, bottom: 0, trailing: 0)) LazyVStack(pinnedViews: [.sectionHeaders]) { + // The header nav bar (username · Dash logo · notifications) + // lives inside HomeBalanceView so the registered-username + // entry point (SB-11) sits in the always-visible balance + // header — reachable without scrolling the feed. HomeBalanceView( viewModel: balanceModel, onLongPress: { @@ -249,7 +292,13 @@ struct HomeViewContent: View { }, onInfo: { network in balanceInfoNetwork = network - }) + }, + username: navUsername, + avatarURL: navAvatarURL, + identitySeed: navIdentitySeed, + hasUnreadNotifications: navHasUnreadNotifications, + onProfileTap: { openProfileFromNav() }, + onNotificationsTap: { openNotificationsFromNav() }) .frame(maxWidth: .infinity) .background(Color.navigationBarColor) .padding(.top, 5) @@ -337,14 +386,40 @@ struct HomeViewContent: View { // the user scrolls down; drives the collapsing top bar. .background( GeometryReader { proxy in - Color.clear.preference( - key: HomeScrollOffsetKey.self, - value: proxy.frame(in: .named("homeScroll")).minY) + Color.clear + .preference( + key: HomeScrollOffsetKey.self, + value: proxy.frame(in: .named("homeScroll")).minY) + .preference(key: HomeContentHeightKey.self, value: proxy.size.height) } ) } .coordinateSpace(name: "homeScroll") + .background( + GeometryReader { proxy in + Color.clear.preference(key: HomeViewportHeightKey.self, value: proxy.size.height) + } + ) + .onPreferenceChange(HomeContentHeightKey.self) { scrollContentHeight = $0 } + .onPreferenceChange(HomeViewportHeightKey.self) { scrollViewportHeight = $0 } .onPreferenceChange(HomeScrollOffsetKey.self) { minY in + // A feed shorter than the viewport can never be scrolled + // `kTopBarShowThreshold` pt, so the reveal gesture below + // would never fire — keep the bar shown so the avatar/ + // username row stays reachable instead of stranded. + // `scrollViewportHeight == 0` means the viewport hasn't + // reported its size yet (first layout pass) — wait for a + // real measurement rather than flash the bar on a feed + // that's actually long enough to scroll normally. + if scrollViewportHeight > 0, + scrollContentHeight - scrollViewportHeight < kTopBarShowThreshold { + if !isTopBarShown { + isTopBarShown = true + delegate?.homeViewDidChangeTopBarVisibility(shouldShow: true) + } + return + } + // minY == 0 at rest; more negative the further down the // user has scrolled. let scrolled = -minY @@ -413,15 +488,88 @@ struct HomeViewContent: View { viewModel.checkJoinDashPay() } #endif + #if DASHPAY + .onReceive(NotificationCenter.default.publisher(for: .DWDashPayRegistrationStatusUpdated)) { _ in + refreshUsernameRow() + } + // Live-refresh the header bell when the contacts snapshot changes + // (a new incoming request / established contact), so unread lights + // up without leaving Home. + .onReceive(NotificationCenter.default.publisher(for: SwiftDashSDKContactsService.contactsDidChangeNotification)) { _ in + refreshUsernameRow() + } + #endif .onAppear { viewModel.checkTimeSkew() #if DASHPAY viewModel.checkJoinDashPay() joinDPViewModel.checkUsername() + refreshUsernameRow() #endif } } + #if DASHPAY + /// Refresh the persistent username row from the app-owned identity + /// snapshot — the same source `HomeViewController.refreshIdentityAvatar()` + /// reads for the nav-bar avatar, so both surfaces always agree. + private func refreshUsernameRow() { + let info = DWCurrentUserIdentityInfo.shared + currentUsername = info.hasIdentity ? info.username : nil + currentAvatarURL = info.avatarURL + currentIdentitySeed = info.identityId ?? Data() + currentHasNotifications = SwiftDashSDKContactsService.shared.unreadNotificationCount > 0 + } + #endif + + // Non-`#if`-gated bridges so the (config-agnostic) HomeBalanceView call + // site stays clean: the header nav-bar inputs and actions resolve to the + // DashPay identity state when compiled with DASHPAY, and to inert + // defaults otherwise. + private var navUsername: String? { + #if DASHPAY + return currentUsername + #else + return nil + #endif + } + + private var navAvatarURL: String? { + #if DASHPAY + return currentAvatarURL + #else + return nil + #endif + } + + private var navIdentitySeed: Data { + #if DASHPAY + return currentIdentitySeed + #else + return Data() + #endif + } + + private var navHasUnreadNotifications: Bool { + #if DASHPAY + return currentHasNotifications + #else + return false + #endif + } + + private func openProfileFromNav() { + #if DASHPAY + delegate?.homeViewEditProfile() + #endif + } + + private func openNotificationsFromNav() { + #if DASHPAY + delegate?.homeViewShowNotifications() + #endif + } + @ViewBuilder private func SectionHeader(key: String, date: Date) -> some View { VStack {