Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
2d12e2d
feat(identities): browsable public keys on the identity detail page
QuantumExplorer Aug 8, 2026
b4415f4
feat(dashpay): Enable DashPay banner for identities missing contact keys
QuantumExplorer Aug 8, 2026
0a8fed2
fix(dashpay): gate add-contact on DashPay keys; banner copy covers send
QuantumExplorer Aug 8, 2026
45d2623
fix(dashpay): Enable DashPay sheet layout and copy
QuantumExplorer Aug 8, 2026
b99a2aa
feat(dashpay): pre-enable DashPay intro with FAQ on the Contacts tab
QuantumExplorer Aug 8, 2026
a97a239
fix(dashpay): honest privacy copy — payments private, requests not yet
QuantumExplorer Aug 8, 2026
91cfd89
fix(dashpay): privacy copy — private but not shielded, chain analysis…
QuantumExplorer Aug 8, 2026
af09ee0
fix(dashpay): intro privacy card copy per owner wording
QuantumExplorer Aug 8, 2026
4c5b61a
feat(dashpay): FAQ comparisons — Bitcoin, Ethereum accounts, name ser…
QuantumExplorer Aug 8, 2026
561d8fa
fix(dashpay): stable FAQ row identity so answers stay open
QuantumExplorer Aug 8, 2026
6e152fa
feat(dashpay): FAQ entry on private contact requests timeline
QuantumExplorer Aug 8, 2026
4599223
fix(dashpay): private-requests FAQ — friendship visibility is a choice
QuantumExplorer Aug 8, 2026
622b478
feat(dashpay): success sheet after enabling with first-request CTA
QuantumExplorer Aug 8, 2026
77d1a6a
fix(dashpay): prominent send-success card in Add Contact
QuantumExplorer Aug 8, 2026
9d08426
fix(dashpay): address PR #941 review findings
QuantumExplorer Aug 8, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,64 @@ final class SwiftDashSDKContactsService: ObservableObject {

// MARK: - Internals

/// How many of the DIP-15 contact-request keys (an enabled ECDSA
/// ENCRYPTION and DECRYPTION key — required on BOTH sides of the ECDH;
/// without them, other users' clients find no recipient key and can't
/// send requests to this identity) the wallet's main identity is
/// missing: 0 (fully enabled), 1, or 2. Drives the Contacts tab's
/// "Enable DashPay" affordance and its fee estimate. Local-store read
/// only — `enableDashPay()` re-checks Platform's authoritative key set
/// before broadcasting anything.
func missingDashPayKeyCount() -> Int {
guard let modelContainer = SwiftDashSDKHost.shared.modelContainer,
let ownerId = DWCurrentUserIdentityInfo.shared.identityId else {
return 0
}
var descriptor = FetchDescriptor<PersistentIdentity>(
predicate: #Predicate { $0.identityId == ownerId })
descriptor.fetchLimit = 1
guard let identity = (try? modelContainer.mainContext.fetch(descriptor))?.first else {
return 0
}
func hasEnabledECDSAKey(purposeRaw: String) -> Bool {
identity.publicKeys.contains { key in
key.purpose == purposeRaw
&& key.keyTypeEnum == .ecdsaSecp256k1
&& !key.isDisabled
}
}
var missing = 0
if !hasEnabledECDSAKey(purposeRaw: String(KeyPurpose.encryption.rawValue)) { missing += 1 }
if !hasEnabledECDSAKey(purposeRaw: String(KeyPurpose.decryption.rawValue)) { missing += 1 }
return missing
}

/// Estimated network fee for the enable-DashPay IdentityUpdate, in
/// duffs (1 duff = 1000 credits): the platform fee schedule's
/// `identity_update` minimum (100,000 credits) plus
/// `identity_key_in_creation_cost` (6,500,000 credits) per added key —
/// rs-platform-version `state_transition_min_fees` v1. The actual fee
/// is computed at execution and deducted from the identity's credit
/// balance; the confirm sheet labels this as an estimate.
static func enableDashPayEstimatedFeeDuffs(missingKeyCount: Int) -> UInt64 {
(100_000 + UInt64(max(missingKeyCount, 1)) * 6_500_000) / 1000
}

/// PIN-gated "Enable DashPay": one IdentityUpdate adding whichever of
/// the ENCRYPTION/DECRYPTION pair the identity is missing on Platform
/// (`DWIdentityKeyUpgrader` — the same lazy repair the contact actions
/// run). No-op returning false when Platform already has both keys.
func enableDashPay() async throws -> Bool {
let (wallet, modelContainer, ownerId) = try requireContext()
try await authorize()
let upgraded = try await ensureOwnDashPayKeys(
wallet: wallet,
ownerId: ownerId,
modelContainer: modelContainer)
Self.logger.info("👥 CONTACTS :: enable DashPay finished (broadcast=\(upgraded, privacy: .public))")
return upgraded
}

private func requireContext() throws -> (ManagedPlatformWallet, ModelContainer, Data) {
guard let wallet = SwiftDashSDKHost.shared.wallet else {
throw ServiceError.noWallet
Expand All @@ -759,12 +817,15 @@ final class SwiftDashSDKContactsService: ObservableObject {
/// `acceptContactRequest` need our own enabled ECDSA ENCRYPTION
/// key for DIP-15 ECDH. No-op once both keys exist; called after
/// the PIN gate so any IdentityUpdate is covered by the same user
/// approval as the contact action it unblocks.
/// approval as the contact action it unblocks. Returns true when
/// an IdentityUpdate was broadcast (the explicit Enable DashPay
/// flow surfaces this; the contact actions ignore it).
@discardableResult
private func ensureOwnDashPayKeys(
wallet: ManagedPlatformWallet,
ownerId: Data,
modelContainer: ModelContainer
) async throws {
) async throws -> Bool {
guard let sdk = SwiftDashSDKHost.shared.sdk,
let network = SwiftDashSDKHost.shared.runningNetwork else {
throw ServiceError.noWallet
Expand All @@ -779,6 +840,7 @@ final class SwiftDashSDKContactsService: ObservableObject {
if upgraded {
Self.logger.info("👥 CONTACTS :: identity upgraded with DashPay keys before contact action")
}
return upgraded
} catch {
Self.logger.error("👥 CONTACTS :: DashPay key upgrade failed: \(String(describing: error), privacy: .public)")
throw ServiceError.sdk(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ struct AddContactScreen: View {
/// send/accept confirmation surface).
@State private var previewTarget: DpnsSearchResult? = nil
@State private var errorMessage: String? = nil
@State private var sentToast = false
/// Username of the recipient of a just-sent request — drives the
/// centered success card (nil = hidden).
@State private var sentToUsername: String?

@ObservedObject private var service = SwiftDashSDKContactsService.shared

Expand Down Expand Up @@ -83,15 +85,26 @@ struct AddContactScreen: View {
onSend: { send(to: target) },
onAccept: { accept(target) })
}
.overlay(alignment: .bottom) {
if sentToast {
Text(NSLocalizedString("Contact request sent", comment: "DashPay Contacts"))
.font(.system(size: 13, weight: .medium))
.padding(.horizontal, 16)
.padding(.vertical, 10)
.background(.thinMaterial, in: Capsule())
.padding(.bottom, 24)
.transition(.move(edge: .bottom).combined(with: .opacity))
.overlay {
if let username = sentToUsername {
VStack(spacing: 12) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 44))
.foregroundColor(.dashGreen)
Text(String(
format: NSLocalizedString("Contact request sent to %@", comment: "DashPay Contacts: success card after sending a request"),
username))
.font(.system(size: 15, weight: .semibold))
.foregroundColor(.dash.primaryText)
.multilineTextAlignment(.center)
}
.padding(28)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.dash.secondaryBackground)
.shadow(color: Color.dash.shadow, radius: 24, x: 0, y: 8))
.padding(.horizontal, 40)
.transition(.scale(scale: 0.9).combined(with: .opacity))
}
}
}
Expand Down Expand Up @@ -321,9 +334,16 @@ struct AddContactScreen: View {
try await service.sendContactRequest(
to: target.identityId,
usernameHint: target.fullName)
withAnimation { sentToast = true }
try? await Task.sleep(nanoseconds: 1_500_000_000)
withAnimation { sentToast = false }
let username = target.fullName.withoutDashSuffix
withAnimation { sentToUsername = username }
try? await Task.sleep(nanoseconds: 2_500_000_000)
// Another send may have replaced the card in the
// meantime — only this recipient's own timer clears it.
withAnimation {
if sentToUsername == username {
sentToUsername = nil
}
}
} catch SwiftDashSDKContactsService.ServiceError.authCancelled {
// User backed out of the PIN prompt.
} catch {
Expand Down
Loading
Loading