mirror of
https://github.com/leonboe1/QuickDrop.git
synced 2026-11-11 02:51:11 +00:00
Add clipboard sync pairing
This commit is contained in:
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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
+6
-6
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
+935
-468
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 {
|
||||
|
||||
+17
-11
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user