diff --git a/NearbyShare/Connection/InboundNearbyConnection.swift b/NearbyShare/Connection/InboundNearbyConnection.swift index e185744..4d6fe26 100644 --- a/NearbyShare/Connection/InboundNearbyConnection.swift +++ b/NearbyShare/Connection/InboundNearbyConnection.swift @@ -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 } } diff --git a/NearbyShare/Connection/NearbyConnectionManager.swift b/NearbyShare/Connection/NearbyConnectionManager.swift index 7ba3a2b..1abccc3 100644 --- a/NearbyShare/Connection/NearbyConnectionManager.swift +++ b/NearbyShare/Connection/NearbyConnectionManager.swift @@ -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 diff --git a/NearbyShare/Connection/OutboundNearbyConnection.swift b/NearbyShare/Connection/OutboundNearbyConnection.swift index 90b6218..c2eeaaa 100644 --- a/NearbyShare/Connection/OutboundNearbyConnection.swift +++ b/NearbyShare/Connection/OutboundNearbyConnection.swift @@ -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() diff --git a/NearbyShare/Model/DataModel.swift b/NearbyShare/Model/DataModel.swift index 5bdfdc8..2e17b7a 100644 --- a/NearbyShare/Model/DataModel.swift +++ b/NearbyShare/Model/DataModel.swift @@ -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" diff --git a/NearbyShare/Util/NotificationSyncPairingToken.swift b/NearbyShare/Util/PairingToken.swift similarity index 82% rename from NearbyShare/Util/NotificationSyncPairingToken.swift rename to NearbyShare/Util/PairingToken.swift index 5ed7d0a..35acd40 100644 --- a/NearbyShare/Util/NotificationSyncPairingToken.swift +++ b/NearbyShare/Util/PairingToken.swift @@ -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 } } diff --git a/NearbyShare/Util/TrustStore.swift b/NearbyShare/Util/TrustStore.swift index 2f5c052..3f9f613 100644 --- a/NearbyShare/Util/TrustStore.swift +++ b/NearbyShare/Util/TrustStore.swift @@ -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 = [] + ) { 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 + + init( + device: RemoteDeviceInfo, + creationDate: Date, + certificateData: Data, + grantedUseCases: Set = [] + ) { + 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.self, forKey: .grantedUseCases) ?? [] + } + + func withGrantedUseCase(_ useCase: PairingUseCase) -> TrustedCertificate { + TrustedCertificate( + device: device, + creationDate: creationDate, + certificateData: certificateData, + grantedUseCases: grantedUseCases.union([useCase]) + ) + } } struct PendingPairingTrust: Equatable { diff --git a/QuickDrop.xcodeproj/project.pbxproj b/QuickDrop.xcodeproj/project.pbxproj index 681b086..3074c28 100644 --- a/QuickDrop.xcodeproj/project.pbxproj +++ b/QuickDrop.xcodeproj/project.pbxproj @@ -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 = ""; }; A1C0DE152F4C000100ABC001 /* ClipboardSyncNotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardSyncNotificationPresenter.swift; sourceTree = ""; }; A1B2C3DA2F33AA0100ABC001 /* MirroredNotificationSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MirroredNotificationSupport.swift; sourceTree = ""; }; - A1F5B3D82F3A1A0100A1B2C3 /* NotificationSyncPairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncPairingToken.swift; sourceTree = ""; }; - A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSyncQrSheetManager.swift; sourceTree = ""; }; + A1F5B3D82F3A1A0100A1B2C3 /* PairingToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingToken.swift; sourceTree = ""; }; + A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingQrSheetManager.swift; sourceTree = ""; }; EB05DFBD2E59FC8000EF567A /* TrustedDevicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedDevicesView.swift; sourceTree = ""; }; EB1325D22E701D8900FD749C /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; EB1325E72E7025D900FD749C /* ActiveNetworkInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveNetworkInterface.swift; sourceTree = ""; }; @@ -763,12 +763,12 @@ path = NearbyShare; sourceTree = ""; }; - A1F5B3DA2F3A1A0100A1B2C5 /* NotificationSync */ = { + A1F5B3DA2F3A1A0100A1B2C5 /* Pairing */ = { isa = PBXGroup; children = ( - A1F5B3D92F3A1A0100A1B2C4 /* NotificationSyncQrSheetManager.swift */, + A1F5B3D92F3A1A0100A1B2C4 /* PairingQrSheetManager.swift */, ); - path = NotificationSync; + path = Pairing; sourceTree = ""; }; 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 = ""; @@ -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 */, diff --git a/QuickDrop/Localizable.xcstrings b/QuickDrop/Localizable.xcstrings index 1edfa36..31b55f9 100644 --- a/QuickDrop/Localizable.xcstrings +++ b/QuickDrop/Localizable.xcstrings @@ -5823,6 +5823,473 @@ } } }, + "ClipboardSyncConsentPromptFromDevice" : { + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "هل تريد تفعيل مزامنة الحافظة مع %1$@؟" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vols activar la sincronització del porta-retalls amb %1$@?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Chcete povolit synchronizaci schránky s %1$@?" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vil du aktivere udklipsholder-synkronisering med %1$@?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Möchtest du die Zwischenablage-Synchronisierung mit %1$@ aktivieren?" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Θέλετε να ενεργοποιήσετε τον συγχρονισμό πρόχειρου με το %1$@;" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you want to enable clipboard sync with %1$@?" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you want to enable clipboard sync with %1$@?" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you want to enable clipboard sync with %1$@?" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you want to enable clipboard sync with %1$@?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres activar la sincronización del portapapeles con %1$@?" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Quieres activar la sincronización del portapapeles con %1$@?" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Haluatko ottaa leikepöydän synkronoinnin käyttöön laitteella %1$@?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voulez-vous activer la synchronisation du presse-papiers avec %1$@ ?" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voulez-vous activer la synchronisation du presse-papiers avec %1$@ ?" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "האם להפעיל סנכרון לוח גזירים עם ‎%1$@?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्या आप %1$@ के साथ क्लिपबोर्ड सिंक सक्षम करना चाहते हैं?" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Želite li omogućiti sinkronizaciju međuspremnika s %1$@?" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Szeretnéd engedélyezni a vágólap szinkronizálását ezzel: %1$@?" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apakah Anda ingin mengaktifkan sinkronisasi clipboard dengan %1$@?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vuoi abilitare la sincronizzazione degli appunti con %1$@?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@ とクリップボード同期を有効にしますか?" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@와(과) 클립보드 동기화를 활성화하시겠습니까?" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Adakah anda ingin mengaktifkan penyegerakan papan klip dengan %1$@?" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vil du aktivere utklippssynk med %1$@?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Wil je klembordsynchronisatie met %1$@ inschakelen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Czy chcesz włączyć synchronizację schowka z %1$@?" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Você deseja ativar a sincronização da área de transferência com %1$@?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pretende ativar a sincronização da área de transferência com %1$@?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Doriți să activați sincronizarea clipboardului cu %1$@?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Вы хотите включить синхронизацию буфера обмена с %1$@?" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Vill du aktivera urklippssynk med %1$@?" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "คุณต้องการเปิดการซิงค์คลิปบอร์ดกับ %1$@ หรือไม่?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pano senkronizasyonunu %1$@ ile etkinleştirmek istiyor musunuz?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Бажаєте увімкнути синхронізацію буфера обміну з %1$@?" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bạn có muốn bật đồng bộ clipboard với %1$@ không?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "你想和 %1$@ 启用剪贴板同步吗?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "你想與 %1$@ 啟用剪貼簿同步嗎?" + } + } + } + }, + "ClipboardSyncToastQrSubtitle" : { + "comment" : "Subtitle in the toast when starting clipboard sync pairing.", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تفعيل مزامنة الحافظة؟" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activar sincronització del porta-retalls?" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Povolit synchronizaci schránky?" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivér udklipsholder-synkronisering?" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zwischenablage-Sync aktivieren?" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ενεργοποίηση συγχρονισμού πρόχειρου;" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Clipboard Sync?" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Clipboard Sync?" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Clipboard Sync?" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enable Clipboard Sync?" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Activar sincronización del portapapeles?" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "¿Activar la sincronización del portapapeles?" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ota leikepöydän synkronointi käyttöön?" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer la synchronisation du presse-papiers ?" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activer la synchro du presse-papiers ?" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "להפעיל סנכרון לוח גזירים?" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "क्लिपबोर्ड सिंक सक्षम करें?" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Omogućiti sinkronizaciju međuspremnika?" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Engedélyezed a vágólap szinkront?" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktifkan Sinkronisasi Clipboard?" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abilitare la sincronizzazione degli appunti?" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "クリップボード同期を有効にしますか?" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "클립보드 동기화를 활성화할까요?" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktifkan Penyegerakan Papan Klip?" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktiver utklippssynk?" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Klembordsynchronisatie inschakelen?" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Włączyć synchronizację schowka?" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ativar sincronização da área de transferência?" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ativar sincronização da área de transferência?" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Activezi sincronizarea clipboardului?" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Включить синхронизацию буфера?" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Aktivera urklippssynk?" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เปิดการซิงค์คลิปบอร์ดหรือไม่?" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pano Senkronizasyonu Etkinleştirilsin mi?" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Увімкнути синхронізацію буфера?" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bật đồng bộ clipboard?" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用剪贴板同步?" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "要啟用剪貼簿同步嗎?" + } + } + } + }, "CloseAlert" : { "extractionState" : "manual", "localizations" : { @@ -24453,474 +24920,6 @@ } } }, - "NotificationSyncQrPairingDescription" : { - "comment" : "Shown under the QR code during notification sync pairing. %1$@ is the device name.", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "امسح الرمز باستخدام %1$@ لإكمال الاقتران." - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escaneja el codi amb %1$@ per completar l'aparellament." - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Naskenujte kód pomocí %1$@ pro dokončení spárování." - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan koden med %1$@ for at fuldføre parringen." - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scanne den Code mit %1$@, um das Koppeln abzuschließen." - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Σαρώστε τον κωδικό με το %1$@ για να ολοκληρώσετε τη σύζευξη." - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan the code with %1$@ to finish pairing." - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan the code with %1$@ to finish pairing." - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan the code with %1$@ to finish pairing." - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan the code with %1$@ to finish pairing." - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escanea el código con %1$@ para finalizar el emparejamiento." - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escanea el código con %1$@ para finalizar el emparejamiento." - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skannaa koodi laitteella %1$@ pariliitoksen viimeistelemiseksi." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scannez le code avec %1$@ pour terminer l’appairage." - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scannez le code avec %1$@ pour terminer l’appariement." - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "סרקו את הקוד עם ‏%1$@ כדי לסיים את ההתאמה." - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पेयरिंग पूरा करने के लिए %1$@ से कोड स्कैन करें।" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skenirajte kod s uređajem %1$@ za dovršetak uparivanja." - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Olvasd be a kódot a(z) %1$@ eszközzel a párosítás befejezéséhez." - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pindai kode dengan %1$@ untuk menyelesaikan pemasangan." - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scansiona il codice con %1$@ per completare l’abbinamento." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ペア設定を完了するには、%1$@でコードをスキャンしてください。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "%1$@로 코드를 스캔하여 페어링을 완료하세요." - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Imbas kod dengan %1$@ untuk selesaikan pemasangan." - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skann koden med %1$@ for å fullføre paringen." - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scan de code met %1$@ om het koppelen te voltooien." - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zeskanuj kod za pomocą %1$@, aby zakończyć parowanie." - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Escaneie o código com %1$@ para concluir o pareamento." - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Digitalize o código com %1$@ para concluir o emparelhamento." - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Scanează codul cu %1$@ pentru a finaliza împerecherea." - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Сканируйте код с помощью %1$@ для завершения соединения." - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Skanna koden med %1$@ för att slutföra parkopplingen." - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "สแกนโค้ดด้วย %1$@ เพื่อเสร็จสิ้นการจับคู่" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eşleştirmeyi tamamlamak için %1$@ ile kodu tarayın." - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Відскануйте код за допомогою %1$@, щоб завершити сполучення." - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Quét mã bằng %1$@ để hoàn tất ghép nối." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "用 %1$@ 扫描二维码以完成配对。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "請使用 %1$@ 掃描此碼以完成配對。" - } - } - } - }, - "NotificationSyncQrPairingTitle" : { - "comment" : "Title for the notification sync QR pairing sheet.", - "extractionState" : "manual", - "localizations" : { - "ar" : { - "stringUnit" : { - "state" : "translated", - "value" : "إكمال الاقتران" - } - }, - "ca" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalitza l'aparellament" - } - }, - "cs" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dokončit párování" - } - }, - "da" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fuldfør parring" - } - }, - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Kopplung abschließen" - } - }, - "el" : { - "stringUnit" : { - "state" : "translated", - "value" : "Ολοκλήρωση ζεύξης" - } - }, - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finish Pairing" - } - }, - "en-AU" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finish Pairing" - } - }, - "en-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finish Pairing" - } - }, - "en-GB" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finish Pairing" - } - }, - "es" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalizar emparejamiento" - } - }, - "es-419" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalizar emparejamiento" - } - }, - "fi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Viimeistele pariliitos" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terminer l’appairage" - } - }, - "fr-CA" : { - "stringUnit" : { - "state" : "translated", - "value" : "Terminer l’appairage" - } - }, - "he" : { - "stringUnit" : { - "state" : "translated", - "value" : "סיום הצימוד" - } - }, - "hi" : { - "stringUnit" : { - "state" : "translated", - "value" : "पेयरिंग पूरी करें" - } - }, - "hr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Dovrši uparivanje" - } - }, - "hu" : { - "stringUnit" : { - "state" : "translated", - "value" : "Párosítás befejezése" - } - }, - "id" : { - "stringUnit" : { - "state" : "translated", - "value" : "Selesaikan pemasangan" - } - }, - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Completa l’abbinamento" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "ペアリングを完了" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "페어링 완료" - } - }, - "ms" : { - "stringUnit" : { - "state" : "translated", - "value" : "Selesaikan pemasangan" - } - }, - "nb" : { - "stringUnit" : { - "state" : "translated", - "value" : "Fullfør paring" - } - }, - "nl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Koppelen voltooien" - } - }, - "pl" : { - "stringUnit" : { - "state" : "translated", - "value" : "Zakończ parowanie" - } - }, - "pt-BR" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalizar pareamento" - } - }, - "pt-PT" : { - "stringUnit" : { - "state" : "translated", - "value" : "Concluir emparelhamento" - } - }, - "ro" : { - "stringUnit" : { - "state" : "translated", - "value" : "Finalizați asocierea" - } - }, - "ru" : { - "stringUnit" : { - "state" : "translated", - "value" : "Завершить сопряжение" - } - }, - "sv" : { - "stringUnit" : { - "state" : "translated", - "value" : "Slutför parkoppling" - } - }, - "th" : { - "stringUnit" : { - "state" : "translated", - "value" : "เสร็จสิ้นการจับคู่" - } - }, - "tr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Eşleştirmeyi bitir" - } - }, - "uk" : { - "stringUnit" : { - "state" : "translated", - "value" : "Завершити сполучення" - } - }, - "vi" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hoàn tất ghép đôi" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "完成配对" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "完成配對" - } - } - } - }, "NotificationSyncSidebar" : { "extractionState" : "manual", "localizations" : { @@ -27018,6 +27017,474 @@ } } }, + "PairingQrSheetDescription" : { + "comment" : "Shown under the QR code during notification sync pairing. %1$@ is the device name.", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "امسح الرمز باستخدام %1$@ لإكمال الاقتران." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escaneja el codi amb %1$@ per completar l'aparellament." + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Naskenujte kód pomocí %1$@ pro dokončení spárování." + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan koden med %1$@ for at fuldføre parringen." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scanne den Code mit %1$@, um das Koppeln abzuschließen." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Σαρώστε τον κωδικό με το %1$@ για να ολοκληρώσετε τη σύζευξη." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan the code with %1$@ to finish pairing." + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan the code with %1$@ to finish pairing." + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan the code with %1$@ to finish pairing." + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan the code with %1$@ to finish pairing." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escanea el código con %1$@ para finalizar el emparejamiento." + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escanea el código con %1$@ para finalizar el emparejamiento." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skannaa koodi laitteella %1$@ pariliitoksen viimeistelemiseksi." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scannez le code avec %1$@ pour terminer l’appairage." + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scannez le code avec %1$@ pour terminer l’appariement." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סרקו את הקוד עם ‏%1$@ כדי לסיים את ההתאמה." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पेयरिंग पूरा करने के लिए %1$@ से कोड स्कैन करें।" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skenirajte kod s uređajem %1$@ za dovršetak uparivanja." + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Olvasd be a kódot a(z) %1$@ eszközzel a párosítás befejezéséhez." + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pindai kode dengan %1$@ untuk menyelesaikan pemasangan." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scansiona il codice con %1$@ per completare l’abbinamento." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペア設定を完了するには、%1$@でコードをスキャンしてください。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "%1$@로 코드를 스캔하여 페어링을 완료하세요." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Imbas kod dengan %1$@ untuk selesaikan pemasangan." + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skann koden med %1$@ for å fullføre paringen." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scan de code met %1$@ om het koppelen te voltooien." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zeskanuj kod za pomocą %1$@, aby zakończyć parowanie." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Escaneie o código com %1$@ para concluir o pareamento." + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Digitalize o código com %1$@ para concluir o emparelhamento." + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scanează codul cu %1$@ pentru a finaliza împerecherea." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Сканируйте код с помощью %1$@ для завершения соединения." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Skanna koden med %1$@ för att slutföra parkopplingen." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "สแกนโค้ดด้วย %1$@ เพื่อเสร็จสิ้นการจับคู่" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eşleştirmeyi tamamlamak için %1$@ ile kodu tarayın." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Відскануйте код за допомогою %1$@, щоб завершити сполучення." + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Quét mã bằng %1$@ để hoàn tất ghép nối." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "用 %1$@ 扫描二维码以完成配对。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "請使用 %1$@ 掃描此碼以完成配對。" + } + } + } + }, + "PairingQrSheetTitle" : { + "comment" : "Title for the notification sync QR pairing sheet.", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إكمال الاقتران" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalitza l'aparellament" + } + }, + "cs" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dokončit párování" + } + }, + "da" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fuldfør parring" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kopplung abschließen" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ολοκλήρωση ζεύξης" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish Pairing" + } + }, + "en-AU" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish Pairing" + } + }, + "en-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish Pairing" + } + }, + "en-GB" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finish Pairing" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar emparejamiento" + } + }, + "es-419" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar emparejamiento" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Viimeistele pariliitos" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminer l’appairage" + } + }, + "fr-CA" : { + "stringUnit" : { + "state" : "translated", + "value" : "Terminer l’appairage" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "סיום הצימוד" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "पेयरिंग पूरी करें" + } + }, + "hr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Dovrši uparivanje" + } + }, + "hu" : { + "stringUnit" : { + "state" : "translated", + "value" : "Párosítás befejezése" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selesaikan pemasangan" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Completa l’abbinamento" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "ペアリングを完了" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "페어링 완료" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "Selesaikan pemasangan" + } + }, + "nb" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fullfør paring" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Koppelen voltooien" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Zakończ parowanie" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizar pareamento" + } + }, + "pt-PT" : { + "stringUnit" : { + "state" : "translated", + "value" : "Concluir emparelhamento" + } + }, + "ro" : { + "stringUnit" : { + "state" : "translated", + "value" : "Finalizați asocierea" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Завершить сопряжение" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "Slutför parkoppling" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "เสร็จสิ้นการจับคู่" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eşleştirmeyi bitir" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Завершити сполучення" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hoàn tất ghép đôi" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成配对" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成配對" + } + } + } + }, "PinCode" : { "extractionState" : "manual", "localizations" : { diff --git a/QuickDrop/Util/ClipboardSyncCoordinator.swift b/QuickDrop/Util/ClipboardSyncCoordinator.swift index f96ca25..9c56472 100644 --- a/QuickDrop/Util/ClipboardSyncCoordinator.swift +++ b/QuickDrop/Util/ClipboardSyncCoordinator.swift @@ -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 { diff --git a/QuickDrop/View/NotificationSync/NotificationSyncQrSheetManager.swift b/QuickDrop/View/Pairing/PairingQrSheetManager.swift similarity index 72% rename from QuickDrop/View/NotificationSync/NotificationSyncQrSheetManager.swift rename to QuickDrop/View/Pairing/PairingQrSheetManager.swift index 167fe8d..43b96bc 100644 --- a/QuickDrop/View/NotificationSync/NotificationSyncQrSheetManager.swift +++ b/QuickDrop/View/Pairing/PairingQrSheetManager.swift @@ -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 { diff --git a/QuickDrop/ViewModel/ReceiveModel.swift b/QuickDrop/ViewModel/ReceiveModel.swift index d56df9c..78585ad 100644 --- a/QuickDrop/ViewModel/ReceiveModel.swift +++ b/QuickDrop/ViewModel/ReceiveModel.swift @@ -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 }