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
6 changes: 6 additions & 0 deletions DashWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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 */; };
Expand Down Expand Up @@ -2747,6 +2749,7 @@
487C4C2E6AC61005536FF85E /* NetworkUnavailableStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkUnavailableStateView.swift; sourceTree = "<group>"; };
48D8BC6623C81DD3F008A668 /* DWPreviewSeedPhraseModel+Mnemonic.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "DWPreviewSeedPhraseModel+Mnemonic.swift"; sourceTree = "<group>"; };
4C4833BE986D59F69A555C5F /* PaymentProtocolMessages.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PaymentProtocolMessages.swift; sourceTree = "<group>"; };
4E55DD28C4EBE049216291A2 /* HomeUsernameRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeUsernameRow.swift; sourceTree = "<group>"; };
4E9C4B39B015CD9D8AD4741D /* SwiftDashSDKWalletRuntime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwiftDashSDKWalletRuntime.swift; sourceTree = "<group>"; };
514AEA122FB324FA00F9B54C /* SwapConvertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConvertView.swift; sourceTree = "<group>"; };
514AEA4A2FB33B4300F9B54C /* SwapConvertHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapConvertHostingController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4330,6 +4333,7 @@
5A1EC0FE2E29A10000000001 /* ShieldedActivityHistory.swift */,
5A1EC0FE2E29A20000000014 /* PlatformAddressHistory.swift */,
75D657662DF579F300ACE570 /* TransactionFilterDialog.swift */,
4E55DD28C4EBE049216291A2 /* HomeUsernameRow.swift */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,33 @@ struct ContactsCard<Content: View>: 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
4 changes: 4 additions & 0 deletions DashWallet/Sources/UI/Home/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,10 @@ extension HomeViewController: HomeViewDelegate {
func homeViewEditProfile() {
profileAction()
}

func homeViewShowNotifications() {
notificationAction()
}
#endif
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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(
Expand Down
62 changes: 62 additions & 0 deletions DashWallet/Sources/UI/Home/Views/HomeUsernameRow.swift
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading