Add clipboard sync pairing

This commit is contained in:
Leon Böttger committed 2026-05-16 14:46:22 +02:00
1 parent 2f6b0ff5ba
commit 72bfd88161
11 files changed
+1118 -537

No files matched your search

@@ -678,6 +678,8 @@ class InboundNearbyConnection: NearbyConnection {
return
}
// Existing trust only proves this is the same device key we trusted before.
// It does not by itself grant a new feature, such as clipboard sync.
let trustedData = TrustStore.shared.findTrustedKey(for: peerCertificate.secretID)
if let trustedData {
guard let currentData = try? peerCertificate.serializedData(),
@@ -685,13 +687,10 @@ class InboundNearbyConnection: NearbyConnection {
self.rejectTransfer(with: .reject)
return
}
DispatchQueue.main.async {
self.delegate?.notificationSyncSetupConfirmed(connection: self)
}
submitUserConsent(accepted: true, trustDevice: false)
return
}
// The QR token proves the user accepted this specific pairing use case.
// Require it even for already-trusted devices before adding a new grant.
let matches = TrustStore.shared.confirmPendingPairingTrust(
secretIdHex: peerCertificate.secretID.hex,
useCase: pairingUseCase,
@@ -703,9 +702,19 @@ class InboundNearbyConnection: NearbyConnection {
return
}
TrustStore.shared.addTrusted(certificate: peerCertificate, device: device)
// Preserve the existing trusted certificate when present; otherwise store
// the newly paired device and attach the confirmed use-case grant.
if trustedData != nil {
TrustStore.shared.grantUseCase(pairingUseCase, for: peerCertificate.secretID.hex)
} else {
TrustStore.shared.addTrusted(
certificate: peerCertificate,
device: device,
grantedUseCases: [pairingUseCase]
)
}
DispatchQueue.main.async {
self.delegate?.notificationSyncSetupConfirmed(connection: self)
self.delegate?.pairingSetupConfirmed(connection: self)
}
submitUserConsent(accepted: true, trustDevice: false)
return
@@ -738,7 +747,22 @@ class InboundNearbyConnection: NearbyConnection {
if let textMetadata = frame.v1.introduction.textMetadata.first {
let isURL = textMetadata.type == .url
textPayloadID = textMetadata.payloadID
isClipboardSyncTransfer = !isURL && isAuthenticated && receiverAuthenticationAccepted
let isRemoteCopy = frame.v1.introduction.hasUseCase
&& frame.v1.introduction.useCase == .remoteCopy
if isRemoteCopy && !isURL {
let peerFingerprint = peerCertificate?.secretID.hex
let isClipboardSyncAllowed = isAuthenticated
&& receiverAuthenticationAccepted
&& TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: peerFingerprint)
guard isClipboardSyncAllowed else {
log("[InboundNearbyConnection \(self.id)] Rejecting clipboard sync transfer because the sender is not trusted for clipboard sync.")
self.rejectTransfer(with: .reject, markAsUserRejected: false)
return
}
isClipboardSyncTransfer = true
} else {
isClipboardSyncTransfer = false
}
let metadata = TransferMetadata(
files: [],
@@ -797,17 +821,15 @@ class InboundNearbyConnection: NearbyConnection {
let pairingUseCase = PairingUseCase(protoValue: pairingMetadata.useCase) {
let secretIdHex = peerCertificate.secretID.hex
if accepted {
if TrustStore.shared.findTrustedKey(for: peerCertificate.secretID) == nil {
if let token = pairingToken {
TrustStore.shared.registerPendingPairingTrust(
secretIdHex: secretIdHex,
pinCode: token,
useCase: pairingUseCase
)
} else {
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
}
if let token = pairingToken {
TrustStore.shared.registerPendingPairingTrust(
secretIdHex: secretIdHex,
pinCode: token,
useCase: pairingUseCase
)
} else {
log("[InboundNearbyConnection \(self.id)] Missing pairing token; pending trust not registered.")
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
}
} else {
TrustStore.shared.clearPendingPairingTrust(secretIdHex: secretIdHex)
@@ -896,7 +918,9 @@ class InboundNearbyConnection: NearbyConnection {
switch useCase {
case .notificationSync:
return .notificationSync
case .clipboardSync, .none:
case .clipboardSync:
return .clipboardSync
case .none:
return nil
}
}
@@ -391,9 +391,9 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
}
}
func notificationSyncSetupConfirmed(connection _: InboundNearbyConnection) {
func pairingSetupConfirmed(connection _: InboundNearbyConnection) {
inboundAppDelegates.forEach { delegate in
delegate.notificationSyncSetupConfirmed()
delegate.pairingSetupConfirmed()
}
}
@@ -770,6 +770,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
urls: [URL],
textToSend: String?,
preparationFinished: (() -> Void)? = nil,
isClipboardSyncTransfer: Bool = false,
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
) -> Bool {
log("Starting outgoing transfer to \(deviceID)")
@@ -808,6 +809,7 @@ public class NearbyConnectionManager: NSObject, NetServiceDelegate, InboundNearb
id: deviceID,
urlsToSend: localUrls,
textToSend: textToSend,
isClipboardSyncTransfer: isClipboardSyncTransfer,
receiverAuthenticationPolicy: receiverAuthenticationPolicy
)
conn.remoteDeviceInfo = info.device
@@ -22,6 +22,7 @@ class OutboundNearbyConnection: NearbyConnection {
private var currentState: State = .initial
private let urlsToSend: [URL]
private let textToSend: String?
private let isClipboardSyncTransfer: Bool
private let receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
private var ukeyClientFinishMsgData: Data?
private var queue: [OutgoingFileTransfer] = []
@@ -42,11 +43,13 @@ class OutboundNearbyConnection: NearbyConnection {
id: String,
urlsToSend: [URL],
textToSend: String?,
isClipboardSyncTransfer: Bool = false,
receiverAuthenticationPolicy: ReceiverAuthenticationPolicy
) {
self.urlsToSend = urlsToSend
self.textToSend = textToSend
self.isClipboardSyncTransfer = isClipboardSyncTransfer
self.receiverAuthenticationPolicy = receiverAuthenticationPolicy
super.init(connection: connection, id: id)
@@ -398,6 +401,9 @@ class OutboundNearbyConnection: NearbyConnection {
var introduction = Sharing_Nearby_Frame()
introduction.version = .v1
introduction.v1.type = .introduction
if isClipboardSyncTransfer {
introduction.v1.introduction.useCase = .remoteCopy
}
if let textToSend = textToSend {
var meta = Sharing_Nearby_TextMetadata()
+11 -4
View File
@@ -19,7 +19,7 @@ protocol InboundNearbyConnectionDelegate {
func updatedTransferProgress(connection: InboundNearbyConnection, progress: Double)
func connectionWasTerminated(connection: InboundNearbyConnection, savedFiles: [URL], error: Error?)
func showPlusScreen()
func notificationSyncSetupConfirmed(connection: InboundNearbyConnection)
func pairingSetupConfirmed(connection: InboundNearbyConnection)
}
@@ -51,11 +51,11 @@ public protocol InboundAppDelegate: AnyObject {
func connectionWasTerminated(connectionID: String, from device: RemoteDeviceInfo?, savedFiles: [URL], wasPlainTextTransfer: Bool, wasClipboardSync: Bool, error: Error?)
func transferProgress(connectionID: String, progress: Double)
func showPlusScreen()
func notificationSyncSetupConfirmed()
func pairingSetupConfirmed()
}
extension InboundAppDelegate {
func notificationSyncSetupConfirmed() {}
func pairingSetupConfirmed() {}
}
@@ -235,6 +235,7 @@ public struct TransferMetadata {
case url
case wifiPassword
case notificationSync
case clipboardSync
}
private func getSummary() -> String {
@@ -298,6 +299,12 @@ public struct TransferMetadata {
return textDescription
}
return "NotificationSyncConsentPromptFromDevice".localized(with: deviceName)
case .clipboardSync:
if let textDescription, !textDescription.isEmpty {
return textDescription
}
return "ClipboardSyncConsentPromptFromDevice".localized(with: deviceName)
}
}
@@ -306,7 +313,7 @@ public struct TransferMetadata {
}
}
enum PairingUseCase: String {
enum PairingUseCase: String, Codable, Hashable {
case notificationSync = "notification_sync"
case clipboardSync = "clipboard_sync"
@@ -1,8 +1,8 @@
//
// NotificationSyncPairingToken.swift
// PairingToken.swift
// QuickDrop
//
// Created by Codex on 2026-03-15.
// Created by Leon Böttger on 2026-03-15.
//
import Foundation
@@ -10,7 +10,7 @@ import QRCode
import SwiftUI
import LUI
enum NotificationSyncPairingToken {
enum PairingToken {
static func generate() -> String {
Data.randomData(length: 16).urlSafeBase64EncodedString()
}
@@ -22,14 +22,14 @@ enum NotificationSyncPairingToken {
return keyId.hex.lowercased()
}
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase = .notificationSync) -> String {
static func qrPayload(token: String, receiverFingerprint: String, useCase: PairingUseCase) -> String {
"quickdrop://pair?token=\(token)&usecase=\(useCase.rawValue)&receiver=\(receiverFingerprint)"
}
static func makeQrImage(
token: String,
receiverFingerprint: String,
useCase: PairingUseCase = .notificationSync,
useCase: PairingUseCase,
foregroundColor: CGColor = CGColor(gray: 0, alpha: 1),
backgroundColor: CGColor = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
) -> Image? {
@@ -47,7 +47,7 @@ enum NotificationSyncPairingToken {
.image(dimension: 1000)
return Image(decorative: qrCodeImage, scale: 1.0, orientation: .up)
} catch {
log("[NotificationSyncPairingToken] QR code generation failed: \(error)")
log("[PairingToken] QR code generation failed: \(error)")
return nil
}
}
+66 -3
View File
@@ -71,17 +71,43 @@ class TrustStore: ObservableObject {
}
func addTrusted(certificate: Sharing_Nearby_PublicCertificate, device: RemoteDeviceInfo) {
func addTrusted(
certificate: Sharing_Nearby_PublicCertificate,
device: RemoteDeviceInfo,
grantedUseCases: Set<PairingUseCase> = []
) {
guard let publicKeyData = try? certificate.serializedData() else { return }
let fingerprint = certificate.secretID.hex
let existingGrants = trustedCertificates[fingerprint]?.grantedUseCases ?? []
trustedCertificates[certificate.secretID.hex] = TrustedCertificate(
trustedCertificates[fingerprint] = TrustedCertificate(
device: device,
creationDate: Date(),
certificateData: publicKeyData
certificateData: publicKeyData,
grantedUseCases: existingGrants.union(grantedUseCases)
)
saveTrustedCertificates()
}
func grantUseCase(_ useCase: PairingUseCase, for secretIdHex: String) {
let normalized = secretIdHex.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard let existing = trustedCertificates[normalized] else { return }
trustedCertificates[normalized] = existing.withGrantedUseCase(useCase)
saveTrustedCertificates()
}
func hasUseCaseGrant(_ useCase: PairingUseCase, for secretIdHex: String?) -> Bool {
guard let normalized = secretIdHex?
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(),
!normalized.isEmpty else {
return false
}
return trustedCertificates[normalized]?.grantedUseCases.contains(useCase) == true
}
func removeTrusted(secretIdHex: String) {
@@ -142,6 +168,43 @@ class TrustStore: ObservableObject {
let device: RemoteDeviceInfo
let creationDate: Date
let certificateData: Data
let grantedUseCases: Set<PairingUseCase>
init(
device: RemoteDeviceInfo,
creationDate: Date,
certificateData: Data,
grantedUseCases: Set<PairingUseCase> = []
) {
self.device = device
self.creationDate = creationDate
self.certificateData = certificateData
self.grantedUseCases = grantedUseCases
}
private enum CodingKeys: String, CodingKey {
case device
case creationDate
case certificateData
case grantedUseCases
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
device = try container.decode(RemoteDeviceInfo.self, forKey: .device)
creationDate = try container.decode(Date.self, forKey: .creationDate)
certificateData = try container.decode(Data.self, forKey: .certificateData)
grantedUseCases = try container.decodeIfPresent(Set<PairingUseCase>.self, forKey: .grantedUseCases) ?? []
}
func withGrantedUseCase(_ useCase: PairingUseCase) -> TrustedCertificate {
TrustedCertificate(
device: device,
creationDate: creationDate,
certificateData: certificateData,
grantedUseCases: grantedUseCases.union([useCase])
)
}
}
struct PendingPairingTrust: Equatable {
+15 -15
View File
@@ -24,10 +24,10 @@
A1B2C3D72F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
A1B2C3D82F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
A1B2C3D92F33AA0100ABC001 /* MirroredNotificationSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */; };
A1F5B3DB2F3A1A0100A1B2C6 /* NotificationSyncPairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */; };
A1F5B3DC2F3A1A0100A1B2C7 /* NotificationSyncPairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */; };
A1F5B3DD2F3A1A0100A1B2C8 /* NotificationSyncQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */; };
A1F5B3DE2F3A1A0100A1B2C9 /* NotificationSyncQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */; };
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */; };
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */; };
EB05DFBE2E59FC8900EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB05DFBF2E59FD0A00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
EB05DFC02E5A2EAA00EF567A /* TrustedDevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */; };
@@ -515,8 +515,8 @@
A1C0DE102F4C000100ABC001 /* ClipboardSyncCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncCoordinator.swift; sourceTree = "<group>"; };
A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = "<group>"; };
A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = "<group>"; };
A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncPairingToken.swift; sourceTree = "<group>"; };
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncQrSheetManager.swift; sourceTree = "<group>"; };
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = "<group>"; };
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = "<group>"; };
EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = "<group>"; };
EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = "<group>"; };
@@ -763,12 +763,12 @@
path = NearbyShare;
sourceTree = "<group>";
};
A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */ = {
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = {
isa = PBXGroup;
children = (
A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */,
A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */,
);
path = NotificationSync;
path = Pairing;
sourceTree = "<group>";
};
EB1325EA2E7025D900FD749C /* SelfTest */ = {
@@ -792,7 +792,7 @@
EB886E512E4E19390052B7D9 /* HKDF.swift */,
EB4F9D232EC27CF000A74A0F /* FileManager+DocumentsOpener.swift */,
EB2F9BFA2DA04B67004010A2 /* Helpers.swift */,
A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */,
A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -925,7 +925,7 @@
EB7654772E525ACC006B4AA3 /* MainMenu.xib */,
EB6E51A92F2F95EE006D9812 /* UserManual */,
EB6E51AA2F2F9606006D9812 /* Toast */,
A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */,
A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */,
EB6E51A82F2F95E0006D9812 /* Settings */,
EB6E51A72F2F95D0006D9812 /* Introduction */,
EBC05C812E33B58300EECC18 /* IssueViews */,
@@ -1363,8 +1363,8 @@
EB16B6152E58B4B9007D9792 /* IdentityManager.swift in Sources */,
EB5C37C42E377A780052EFD7 /* securegcm.pb.swift in Sources */,
EB97A0102E51CD13008B8C5C /* ReceiveModel.swift in Sources */,
A1F5B3DD2F3A1A0100A1B2C8 /* NotificationSyncQrSheetManager.swift in Sources */,
A1F5B3DB2F3A1A0100A1B2C6 /* NotificationSyncPairingToken.swift in Sources */,
A1F5B3DD2F3A1A0100A1B2C8 /* PairingQrSheetManager.swift in Sources */,
A1F5B3DB2F3A1A0100A1B2C6 /* PairingToken.swift in Sources */,
EB5C37CB2E377A7F0052EFD7 /* NearbyConnection.swift in Sources */,
EB5C37CC2E377A7F0052EFD7 /* DataModel.swift in Sources */,
EB5C37CD2E377A7F0052EFD7 /* OutboundNearbyConnection.swift in Sources */,
@@ -1423,8 +1423,8 @@
EB3B9F112EF7150400D6704E /* IdentityManager.swift in Sources */,
EB3B9F122EF7150400D6704E /* securegcm.pb.swift in Sources */,
EB3B9F132EF7150400D6704E /* ReceiveModel.swift in Sources */,
A1F5B3DE2F3A1A0100A1B2C9 /* NotificationSyncQrSheetManager.swift in Sources */,
A1F5B3DC2F3A1A0100A1B2C7 /* NotificationSyncPairingToken.swift in Sources */,
A1F5B3DE2F3A1A0100A1B2C9 /* PairingQrSheetManager.swift in Sources */,
A1F5B3DC2F3A1A0100A1B2C7 /* PairingToken.swift in Sources */,
EB3B9F142EF7150400D6704E /* NearbyConnection.swift in Sources */,
EB3B9F152EF7150400D6704E /* DataModel.swift in Sources */,
EB3B9F162EF7150400D6704E /* OutboundNearbyConnection.swift in Sources */,
File diff suppressed because it is too large. Load diff
@@ -134,6 +134,7 @@ final class ClipboardSyncCoordinator {
delegate: delegate,
urls: [],
textToSend: clipboardText,
isClipboardSyncTransfer: true,
receiverAuthenticationPolicy: .trustedReceiver(fingerprint: target.fingerprint)
)
}
@@ -174,6 +175,7 @@ final class ClipboardSyncCoordinator {
!deviceID.isEmpty,
let fingerprint = device.keyFingerprint,
trustedFingerprints.contains(fingerprint),
TrustStore.shared.hasUseCaseGrant(.clipboardSync, for: fingerprint),
device.supportsClipboardReceive,
device.type != .computer,
seenFingerprints.insert(fingerprint).inserted else {
@@ -1,33 +1,39 @@
//
// NotificationSyncQrSheetManager.swift
// PairingQrSheetManager.swift
// QuickDrop
//
// Created by Codex on 2026-03-15.
// Created by Leon Böttger on 2026-03-15.
//
import AppKit
import SwiftUI
final class NotificationSyncQrSheetManager: NSObject, NSWindowDelegate {
static let shared = NotificationSyncQrSheetManager()
final class PairingQrSheetManager: NSObject, NSWindowDelegate {
static let shared = PairingQrSheetManager()
private var alert: NSAlert?
private weak var alertWindow: NSWindow?
func open(token: String, receiverFingerprint: String, deviceName: String) {
func open(
token: String,
receiverFingerprint: String,
deviceName: String,
useCase: PairingUseCase
) {
guard alert == nil else { return }
guard let qrImage = NotificationSyncPairingToken.makeQrImage(
guard let qrImage = PairingToken.makeQrImage(
token: token,
receiverFingerprint: receiverFingerprint
receiverFingerprint: receiverFingerprint,
useCase: useCase
) else { return }
let alert = NSAlert()
alert.messageText = "NotificationSyncQrPairingTitle".localized()
alert.informativeText = "NotificationSyncQrPairingDescription".localized(with: deviceName)
alert.messageText = "PairingQrSheetTitle".localized()
alert.informativeText = "PairingQrSheetDescription".localized(with: deviceName)
alert.addButton(withTitle: "Cancel".localized())
alert.alertStyle = .informational
let accessoryView = NSHostingView(rootView: NotificationSyncQrAccessoryView(qrImage: qrImage))
let accessoryView = NSHostingView(rootView: PairingQrAccessoryView(qrImage: qrImage))
accessoryView.frame = NSRect(x: 0, y: 0, width: 180, height: 180)
alert.accessoryView = accessoryView
alert.layout()
@@ -71,7 +77,7 @@ final class NotificationSyncQrSheetManager: NSObject, NSWindowDelegate {
}
}
private struct NotificationSyncQrAccessoryView: View {
private struct PairingQrAccessoryView: View {
let qrImage: Image
var body: some View {
+12 -8
View File
@@ -84,7 +84,7 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
#if os(macOS)
DispatchQueue.main.async {
if transfer.type == .notificationSync {
if transfer.type == .notificationSync || transfer.type == .clipboardSync {
self.hideQuickDropToast()
self.toastActions = nil
self.progress = nil
@@ -93,7 +93,10 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
let resolvedSenderName = (senderName?.isEmpty == false) ? senderName! : "AndroidDevice".localized()
self.activeDeviceName = resolvedSenderName
let compareMessage = "NotificationSyncToastQrSubtitle".localized()
let pairingUseCase: PairingUseCase = transfer.type == .clipboardSync ? .clipboardSync : .notificationSync
let compareMessage = transfer.type == .clipboardSync
? "ClipboardSyncToastQrSubtitle".localized()
: "NotificationSyncToastQrSubtitle".localized()
self.consentState = ConsentToastState(
transferID: transferID,
pinCodeMessage: "",
@@ -105,17 +108,18 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
withAnimation {
self.consentState = nil
}
guard let receiverFingerprint = NotificationSyncPairingToken.receiverFingerprintHex() else {
guard let receiverFingerprint = PairingToken.receiverFingerprintHex() else {
log("[ReceiveModel] Missing local receiver fingerprint; cannot start notification sync pairing.")
secondaryButtonAction()
self.hideQuickDropToast(style: .fade)
return
}
let pairingToken = NotificationSyncPairingToken.generate()
NotificationSyncQrSheetManager.shared.open(
let pairingToken = PairingToken.generate()
PairingQrSheetManager.shared.open(
token: pairingToken,
receiverFingerprint: receiverFingerprint,
deviceName: resolvedSenderName
deviceName: resolvedSenderName,
useCase: pairingUseCase
)
NearbyConnectionManager.shared.submitUserConsent(
transferID: transferID,
@@ -237,13 +241,13 @@ class ReceiveModel: ObservableObject, InboundAppDelegate {
}
func notificationSyncSetupConfirmed() {
func pairingSetupConfirmed() {
#if os(macOS)
DispatchQueue.main.async {
if self.consentState?.notificationSyncStage != nil {
self.hideQuickDropToast(style: .fade)
}
NotificationSyncQrSheetManager.shared.close()
PairingQrSheetManager.shared.close()
}
#endif
}