diff --git a/Bitwarden/Application/SceneDelegate.swift b/Bitwarden/Application/SceneDelegate.swift index 698c4b4b8..2a6705775 100644 --- a/Bitwarden/Application/SceneDelegate.swift +++ b/Bitwarden/Application/SceneDelegate.swift @@ -77,7 +77,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { #if SUPPORTS_CXP - if #available(iOS 18.2, *), + if #available(iOS 26.0, *), let userActivity = connectionOptions.userActivities.first { await checkAndHandleCredentialExchangeActivity(appProcessor: appProcessor, userActivity: userActivity) } @@ -101,7 +101,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { #if SUPPORTS_CXP - if #available(iOS 18.2, *) { + if #available(iOS 26.0, *) { Task { await checkAndHandleCredentialExchangeActivity(appProcessor: appProcessor, userActivity: userActivity) } @@ -182,11 +182,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { #endif } -// MARK: - SceneDelegate 18.2 +// MARK: - SceneDelegate 26.0 #if SUPPORTS_CXP -@available(iOS 18.2, *) +@available(iOS 26.0, *) extension SceneDelegate { /// Checks whether there is an `ASCredentialExchangeActivity` in the `userActivity` and handles it. /// - Parameters: diff --git a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoder+Bitwarden.swift b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoder+Bitwarden.swift index 4283e0242..18d475d38 100644 --- a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoder+Bitwarden.swift +++ b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoder+Bitwarden.swift @@ -59,8 +59,7 @@ public extension JSONDecoder { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .custom { keys in let key = keys.last!.stringValue - let camelCaseKey = keyToCamelCase(key: key) - return AnyKey(stringValue: customTransformCodingKeyForCXF(key: camelCaseKey)) + return AnyKey(stringValue: keyToCamelCase(key: key)) } decoder.dateDecodingStrategy = .secondsSince1970 return decoder @@ -82,23 +81,4 @@ public extension JSONDecoder { // Handle PascalCase or camelCase. return key.prefix(1).lowercased() + key.dropFirst() } - - // MARK: Private Static Functions - - /// Transforms the keys from Credential Exchange format handled by the Bitwarden SDK - /// into the keys that Apple expects. - private static func customTransformCodingKeyForCXF(key: String) -> String { - guard #available(iOS 18.3, *) else { - return switch key { - case "credentialId": - "credentialID" - case "rpId": - "rpID" - default: - key - } - } - - return key - } } diff --git a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoderBitwardenTests.swift b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoderBitwardenTests.swift index 8ae3bda6c..11d7bcc72 100644 --- a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoderBitwardenTests.swift +++ b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONDecoderBitwardenTests.swift @@ -8,10 +8,6 @@ class JSONDecoderBitwardenTests: BitwardenTestCase { /// `JSONDecoder.cxfDecoder` can decode Credential Exchange Format. func test_cxfDecoder_decodesISO8601DateWithFractionalSeconds() throws { - guard #available(iOS 18.3, *) else { - throw XCTSkip("This test only works from iOS 18.3") - } - let subject = JSONDecoder.cxfDecoder let toDecode = #"{"credentialId":"credential","date":1697790414,"otherKey":"other","rpId":"rp"}"# @@ -36,36 +32,6 @@ class JSONDecoderBitwardenTests: BitwardenTestCase { ) } - /// `JSONDecoder.cxfDecoder` can decode Credential Exchange Format on iOS versions lower than iOS 18.3. - func test_cxfDecoder_decodesISO8601DateWithFractionalSecondsOlderiOS18dot3() throws { - if #available(iOS 18.3, *) { - throw XCTSkip("This test only works until iOS 18.3") - } - - let subject = JSONDecoder.cxfDecoder - let toDecode = #"{"credentialId":"credential","date":1697790414,"otherKey":"other","rpId":"rp"}"# - - struct JSONBody: Codable, Equatable { - let credentialID: String - let date: Date - let otherKey: String - let rpID: String - } - - let body = JSONBody( - credentialID: "credential", - date: Date(year: 2023, month: 10, day: 20, hour: 8, minute: 26, second: 54), - otherKey: "other", - rpID: "rp" - ) - - XCTAssertEqual( - try subject - .decode(JSONBody.self, from: Data(toDecode.utf8)), - body - ) - } - /// `JSONDecoder.defaultDecoder` can decode ISO8601 dates with fractional seconds. func test_defaultDecoder_decodesISO8601DateWithFractionalSeconds() { let subject = JSONDecoder.defaultDecoder diff --git a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoder+Bitwarden.swift b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoder+Bitwarden.swift index 9bf2a4dd8..58f029294 100644 --- a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoder+Bitwarden.swift +++ b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoder+Bitwarden.swift @@ -23,25 +23,6 @@ public extension JSONEncoder { var container = encoder.singleValueContainer() try container.encode(Int(date.timeIntervalSince1970)) } - jsonEncoder.keyEncodingStrategy = .custom { keys in - let key = keys.last!.stringValue - return AnyKey(stringValue: customTransformCodingKeyForCXF(key: key)) - } return jsonEncoder }() - - // MARK: Static Functions - - /// Transforms the keys from Credential Exchange format handled by the Bitwarden SDK - /// into the keys that Apple expects. - static func customTransformCodingKeyForCXF(key: String) -> String { - return switch key { - case "credentialID": - "credentialId" - case "rpID": - "rpId" - default: - key - } - } } diff --git a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoderBitwardenTests.swift b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoderBitwardenTests.swift index e00223acb..4cbedaeda 100644 --- a/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoderBitwardenTests.swift +++ b/BitwardenKit/Core/Platform/Services/API/Extensions/JSONEncoderBitwardenTests.swift @@ -12,17 +12,17 @@ class JSONEncoderBitwardenTests: BitwardenTestCase { subject.outputFormatting = .sortedKeys // added for test consistency so output is ordered. struct JSONBody: Codable { - let credentialID: String + let credentialId: String let date: Date let otherKey: String - let rpID: String + let rpId: String } let body = JSONBody( - credentialID: "credential", + credentialId: "credential", date: Date(year: 2023, month: 10, day: 20, hour: 8, minute: 26, second: 54), otherKey: "other", - rpID: "rp" + rpId: "rp" ) let encodedData = try subject.encode(body) let encodedString = String(data: encodedData, encoding: .utf8) diff --git a/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepository.swift b/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepository.swift index 17b0886ac..2f25e8cba 100644 --- a/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepository.swift +++ b/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepository.swift @@ -15,8 +15,8 @@ protocol ExportCXFCiphersRepository { /// Export the credentials using the Credential Exchange flow. /// /// - Parameter data: Data to export. - @available(iOS 18.2, *) - func exportCredentials(data: ASImportableAccount, presentationAnchor: () -> ASPresentationAnchor) async throws + @available(iOS 26.0, *) + func exportCredentials(data: ASImportableAccount, presentationAnchor: () async -> ASPresentationAnchor) async throws #endif /// Gets all ciphers to export in Credential Exchange flow. @@ -28,7 +28,7 @@ protocol ExportCXFCiphersRepository { /// Exports the vault creating the `ASImportableAccount` to be used in Credential Exchange Protocol. /// /// - Returns: An `ASImportableAccount` - @available(iOS 18.2, *) + @available(iOS 26.0, *) func getExportVaultDataForCXF() async throws -> ASImportableAccount #endif } @@ -93,10 +93,30 @@ class DefaultExportCXFCiphersRepository: ExportCXFCiphersRepository { #if SUPPORTS_CXP - @available(iOS 18.2, *) - func exportCredentials(data: ASImportableAccount, presentationAnchor: () -> ASPresentationAnchor) async throws { - try await credentialManagerFactory.createExportManager(presentationAnchor: presentationAnchor()) - .exportCredentials(ASExportedCredentialData(accounts: [data])) + @available(iOS 26.0, *) + func exportCredentials( + data: ASImportableAccount, + presentationAnchor: () async -> ASPresentationAnchor + ) async throws { + let manager = await credentialManagerFactory.createExportManager(presentationAnchor: presentationAnchor()) + + let options = try await manager.requestExport(forExtensionBundleIdentifier: nil) + guard let exportOptions = options as? ASCredentialExportManager.ExportOptions else { + throw BitwardenError.generalError( + type: "Wrong export options", + message: "The credential manager returned wrong export options type." + ) + } + + try await manager.exportCredentials( + ASExportedCredentialData( + accounts: [data], + formatVersion: exportOptions.formatVersion, + exporterRelyingPartyIdentifier: Bundle.main.appIdentifier, + exporterDisplayName: "Bitwarden", + timestamp: Date.now + ) + ) } #endif @@ -108,7 +128,7 @@ class DefaultExportCXFCiphersRepository: ExportCXFCiphersRepository { #if SUPPORTS_CXP - @available(iOS 18.2, *) + @available(iOS 26.0, *) func getExportVaultDataForCXF() async throws -> ASImportableAccount { let ciphers = try await getAllCiphersToExportCXF() diff --git a/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepositoryTests.swift b/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepositoryTests.swift index 61c079f31..5c96466bc 100644 --- a/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepositoryTests.swift +++ b/BitwardenShared/Core/Tools/Repositories/ExportCXFCiphersRepositoryTests.swift @@ -77,8 +77,8 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase { /// `exportCredentials(data:presentationAnchor:)` exports the credential data. @MainActor func test_exportCredentials() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Exporting ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Exporting ciphers requires iOS 26.0") } let exportManager = MockCredentialExportManager() credentialManagerFactory.exportManager = exportManager @@ -90,8 +90,8 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase { /// `exportCredentials(data:presentationAnchor:)` throws when exporting. @MainActor func test_exportCredentials_throws() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Exporting ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Exporting ciphers requires iOS 26.0") } let exportManager = MockCredentialExportManager() exportManager.exportCredentialsError = BitwardenTestError.example @@ -131,8 +131,8 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase { /// `getExportVaultDataForCXF()` gets the vault data prepared for export on CXF. @MainActor func test_getExportVaultDataForCXF() async throws { // swiftlint:disable:this function_body_length - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } cipherService.fetchAllCiphersResult = .success([ .fixture(id: "1"), @@ -192,8 +192,8 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase { /// `getExportVaultDataForCXF()` throws when getting all ciphers to export. @MainActor func test_getExportVaultDataForCXF_throwsGettingCiphers() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } cipherService.fetchAllCiphersResult = .failure(BitwardenTestError.example) @@ -205,8 +205,8 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase { /// `getExportVaultDataForCXF()` throws when getting account. @MainActor func test_getExportVaultDataForCXF_throwsAccount() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } cipherService.fetchAllCiphersResult = .success([ .fixture(id: "1"), @@ -221,8 +221,8 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase { /// `getExportVaultDataForCXF()` throws when exporting using the SDK. @MainActor func test_getExportVaultDataForCXF_throwsExporting() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } cipherService.fetchAllCiphersResult = .success([ .fixture(id: "1"), diff --git a/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepository.swift b/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepository.swift index da1ed20d5..f52d94891 100644 --- a/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepository.swift +++ b/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepository.swift @@ -11,7 +11,7 @@ protocol ImportCiphersRepository: AnyObject { /// - onProgress: Closure to update progress. /// - Returns: A dictionary containing the localized cipher type (key) and count (value) of that type /// that was imported, e.g. ["Passwords": 3, "Cards": 2]. - @available(iOS 18.2, *) + @available(iOS 26.0, *) func importCiphers( credentialImportToken: UUID, onProgress: @MainActor (Double) -> Void @@ -69,7 +69,7 @@ class DefaultImportCiphersRepository { // MARK: ImportCiphersRepository extension DefaultImportCiphersRepository: ImportCiphersRepository { - @available(iOS 18.2, *) + @available(iOS 26.0, *) func importCiphers( credentialImportToken: UUID, onProgress: @MainActor (Double) -> Void diff --git a/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepositoryTests.swift b/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepositoryTests.swift index ea3ee211c..5e4c8cf07 100644 --- a/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepositoryTests.swift +++ b/BitwardenShared/Core/Tools/Repositories/ImportCiphersRepositoryTests.swift @@ -53,18 +53,16 @@ class ImportCiphersRepositoryTests: BitwardenTestCase { /// updates progress report and returns the credentials result with each type count. @MainActor func test_importCiphers_success() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Importing ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Importing ciphers requires iOS 26.0") } let credentialImportManager = MockCredentialImportManager() credentialImportManager.importCredentialsResult = try .success(getASExportedCredentialDataAsJson( - data: ASExportedCredentialData( - accounts: [ - .fixture(items: [.fixture()]), - ] - ) + accounts: [ + .fixture(items: [.fixture()]), + ] )) credentialManagerFactory.importManager = credentialImportManager @@ -113,16 +111,14 @@ class ImportCiphersRepositoryTests: BitwardenTestCase { /// when there are no accounts after importing credentials. @MainActor func test_importCiphers_noDataFound() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Importing ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Importing ciphers requires iOS 26.0") } let credentialImportManager = MockCredentialImportManager() credentialImportManager.importCredentialsResult = try .success(getASExportedCredentialDataAsJson( - data: ASExportedCredentialData( - accounts: [] - ) + accounts: [] )) credentialManagerFactory.importManager = credentialImportManager @@ -140,18 +136,16 @@ class ImportCiphersRepositoryTests: BitwardenTestCase { /// the SDK to import the ciphers. @MainActor func test_importCiphers_sdkThrows() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Importing ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Importing ciphers requires iOS 26.0") } let credentialImportManager = MockCredentialImportManager() credentialImportManager.importCredentialsResult = try .success(getASExportedCredentialDataAsJson( - data: ASExportedCredentialData( - accounts: [ - .fixture(items: [.fixture()]), - ] - ) + accounts: [ + .fixture(items: [.fixture()]), + ] )) credentialManagerFactory.importManager = credentialImportManager @@ -171,18 +165,16 @@ class ImportCiphersRepositoryTests: BitwardenTestCase { /// to import the ciphers. @MainActor func test_importCiphers_throwsWhenImportingCiphersAPI() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Importing ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Importing ciphers requires iOS 26.0") } let credentialImportManager = MockCredentialImportManager() credentialImportManager.importCredentialsResult = try .success(getASExportedCredentialDataAsJson( - data: ASExportedCredentialData( - accounts: [ - .fixture(items: [.fixture()]), - ] - ) + accounts: [ + .fixture(items: [.fixture()]), + ] )) credentialManagerFactory.importManager = credentialImportManager @@ -209,18 +201,16 @@ class ImportCiphersRepositoryTests: BitwardenTestCase { /// importing the ciphers. @MainActor func test_importCiphers_throwsWhenSyncing() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("Importing ciphers requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("Importing ciphers requires iOS 26.0") } let credentialImportManager = MockCredentialImportManager() credentialImportManager.importCredentialsResult = try .success(getASExportedCredentialDataAsJson( - data: ASExportedCredentialData( - accounts: [ - .fixture(items: [.fixture()]), - ] - ) + accounts: [ + .fixture(items: [.fixture()]), + ] )) credentialManagerFactory.importManager = credentialImportManager @@ -245,8 +235,15 @@ class ImportCiphersRepositoryTests: BitwardenTestCase { // MARK: Private - @available(iOS 18.2, *) - private func getASExportedCredentialDataAsJson(data: ASExportedCredentialData) throws -> String { + @available(iOS 26.0, *) + private func getASExportedCredentialDataAsJson(accounts: [ASImportableAccount]) throws -> String { + let data = ASExportedCredentialData( + accounts: accounts, + formatVersion: .v1, + exporterRelyingPartyIdentifier: "com.bitwarden.test", + exporterDisplayName: "Bitwarden Test", + timestamp: .now + ) let credentialData = try JSONEncoder.cxfEncoder.encode(data) guard let credentialDataJsonString = String(data: credentialData, encoding: .utf8) else { throw BitwardenError.dataError("Failed to encode ASExportedCredentialData") diff --git a/BitwardenShared/Core/Tools/Repositories/TestHelpers/MockExportCXFCiphersRepository.swift b/BitwardenShared/Core/Tools/Repositories/TestHelpers/MockExportCXFCiphersRepository.swift index ff8ac7d0f..e6bd1bcc3 100644 --- a/BitwardenShared/Core/Tools/Repositories/TestHelpers/MockExportCXFCiphersRepository.swift +++ b/BitwardenShared/Core/Tools/Repositories/TestHelpers/MockExportCXFCiphersRepository.swift @@ -21,8 +21,11 @@ class MockExportCXFCiphersRepository: ExportCXFCiphersRepository { #if SUPPORTS_CXP - @available(iOS 18.2, *) - func exportCredentials(data: ASImportableAccount, presentationAnchor: () -> ASPresentationAnchor) async throws { + @available(iOS 26.0, *) + func exportCredentials( + data: ASImportableAccount, + presentationAnchor: () async -> ASPresentationAnchor + ) async throws { exportCredentialsData = data if let exportCredentialsError { throw exportCredentialsError @@ -37,7 +40,7 @@ class MockExportCXFCiphersRepository: ExportCXFCiphersRepository { #if SUPPORTS_CXP - @available(iOS 18.2, *) + @available(iOS 26.0, *) func getExportVaultDataForCXF() async throws -> ASImportableAccount { guard let result = try getExportVaultDataForCXFResult.get() as? ASImportableAccount else { throw MockExportCXFCiphersRepositoryError.unableToCastToASImportableAccount @@ -51,7 +54,7 @@ class MockExportCXFCiphersRepository: ExportCXFCiphersRepository { protocol ImportableAccountProxy {} #if SUPPORTS_CXP -@available(iOS 18.2, *) +@available(iOS 26.0, *) extension ASImportableAccount: ImportableAccountProxy {} #endif diff --git a/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactory.swift b/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactory.swift index a92b7400e..f52b6ef33 100644 --- a/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactory.swift +++ b/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactory.swift @@ -4,38 +4,50 @@ import Foundation // MARK: - CredentialManagerFactory protocol CredentialManagerFactory { - @available(iOS 18.2, *) + @available(iOS 26.0, *) func createExportManager(presentationAnchor: ASPresentationAnchor) -> CredentialExportManager - @available(iOS 18.2, *) + @available(iOS 26.0, *) func createImportManager() -> CredentialImportManager } // MARK: - DefaultCredentialManagerFactory struct DefaultCredentialManagerFactory: CredentialManagerFactory { - @available(iOS 18.2, *) + @available(iOS 26.0, *) func createExportManager(presentationAnchor: ASPresentationAnchor) -> any CredentialExportManager { ASCredentialExportManager(presentationAnchor: presentationAnchor) } - @available(iOS 18.2, *) + @available(iOS 26.0, *) func createImportManager() -> any CredentialImportManager { ASCredentialImportManager() } } +// MARK: - CredentialExportManagerExportOptions + +protocol CredentialExportManagerExportOptions {} + +// MARK: - ASCredentialExportManager.ExportOptions + +@available(iOS 26.0, *) +extension ASCredentialExportManager.ExportOptions: CredentialExportManagerExportOptions {} + // MARK: - CredentialExportManager protocol CredentialExportManager: AnyObject { - @available(iOS 18.2, *) + @available(iOS 26.0, *) func exportCredentials(_ credentialData: ASExportedCredentialData) async throws + + @available(iOS 26.0, *) + func requestExport(forExtensionBundleIdentifier: String?) async throws -> CredentialExportManagerExportOptions } // MARK: - CredentialImportManager protocol CredentialImportManager: AnyObject { - @available(iOS 18.2, *) + @available(iOS 26.0, *) func importCredentials(token: UUID) async throws -> ASExportedCredentialData } @@ -46,10 +58,14 @@ protocol CredentialImportManager: AnyObject { #if SUPPORTS_CXP -@available(iOS 18.2, *) -extension ASCredentialExportManager: CredentialExportManager {} +@available(iOS 26.0, *) +extension ASCredentialExportManager: CredentialExportManager { + func requestExport(forExtensionBundleIdentifier: String?) async throws -> any CredentialExportManagerExportOptions { + try await requestExport(for: forExtensionBundleIdentifier) + } +} -@available(iOS 18.2, *) +@available(iOS 26.0, *) extension ASCredentialImportManager: CredentialImportManager {} #else @@ -61,9 +77,16 @@ class ASCredentialImportManager: CredentialImportManager { } class ASCredentialExportManager: CredentialExportManager { + struct ExportOptions: CredentialExportManagerExportOptions {} + init(presentationAnchor: ASPresentationAnchor) {} func exportCredentials(_ credentialData: ASExportedCredentialData) async throws {} + + @available(iOS 26.0, *) + func requestExport(forExtensionBundleIdentifier: String?) async throws -> CredentialExportManagerExportOptions { + ASCredentialExportManager.ExportOptions() + } } struct ASExportedCredentialData {} diff --git a/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactoryTests.swift b/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactoryTests.swift index 3df423d1c..08af049c0 100644 --- a/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactoryTests.swift +++ b/BitwardenShared/Core/Tools/Utilities/CredentialManagerFactoryTests.swift @@ -31,8 +31,8 @@ class CredentialManagerFactoryTests: BitwardenTestCase { /// `createExportManager(presentationAnchor:)` creates an instance of the credential export manager. @MainActor func test_createExportManager() throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("iOS 18.2 is required to run this test.") + guard #available(iOS 26.0, *) else { + throw XCTSkip("iOS 26.0 is required to run this test.") } let manager = subject.createExportManager(presentationAnchor: UIWindow()) @@ -41,8 +41,8 @@ class CredentialManagerFactoryTests: BitwardenTestCase { /// `createImportManager()` creates an instance of the credential import manager. func test_createImportManager() throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("iOS 18.2 is required to run this test.") + guard #available(iOS 26.0, *) else { + throw XCTSkip("iOS 26.0 is required to run this test.") } let manager = subject.createImportManager() diff --git a/BitwardenShared/Core/Tools/Utilities/TestHelpers/MockCredentialManagerFactory.swift b/BitwardenShared/Core/Tools/Utilities/TestHelpers/MockCredentialManagerFactory.swift index f087c0289..0fdef4d08 100644 --- a/BitwardenShared/Core/Tools/Utilities/TestHelpers/MockCredentialManagerFactory.swift +++ b/BitwardenShared/Core/Tools/Utilities/TestHelpers/MockCredentialManagerFactory.swift @@ -9,12 +9,12 @@ class MockCredentialManagerFactory: CredentialManagerFactory { var exportManager: CredentialExportManager? var importManager: CredentialImportManager? - @available(iOS 18.2, *) + @available(iOS 26.0, *) func createExportManager(presentationAnchor: ASPresentationAnchor) -> any CredentialExportManager { exportManager ?? MockCredentialExportManager() } - @available(iOS 18.2, *) + @available(iOS 26.0, *) func createImportManager() -> CredentialImportManager { importManager ?? MockCredentialImportManager() } @@ -24,13 +24,16 @@ class MockCredentialExportManager: CredentialExportManager { var exportCredentialsCalled = false /// The data passed as parameter in `exportCredentials(_:)`. /// A JSON encoded `String` is used as the value instead of the actual object - /// to avoid crashing on simulators older than iOS 18.2 because of not finding the symbol + /// to avoid crashing on simulators older than iOS 26.0 because of not finding the symbol /// thus resulting in bad access error when running the test suite. /// Use `JSONDecoder.cxfDecoder.decode` to decode data for this. var exportCredentialsJSONData: String? var exportCredentialsError: Error? + var requestExportResult: Result = .success( + MockCredentialExportManagerExportOptions() + ) - @available(iOS 18.2, *) + @available(iOS 26.0, *) func exportCredentials(_ credentialData: ASExportedCredentialData) async throws { exportCredentialsCalled = true @@ -45,17 +48,22 @@ class MockCredentialExportManager: CredentialExportManager { throw exportCredentialsError } } + + @available(iOS 26.0, *) + func requestExport(forExtensionBundleIdentifier: String?) async throws -> CredentialExportManagerExportOptions { + try requestExportResult.get() + } } class MockCredentialImportManager: CredentialImportManager { /// The result of calling `importCredentials(token:)`. /// A JSON encoded `String` is used as the value instead of the actual object - /// to avoid crashing on simulators older than iOS 18.2 because of not finding the symbol + /// to avoid crashing on simulators older than iOS 26.0 because of not finding the symbol /// thus resulting in bad access error when running the test suite. /// Use `JSONEncoder.cxfEncoder.encode` to encode data for this. var importCredentialsResult: Result = .failure(BitwardenTestError.example) - @available(iOS 18.2, *) + @available(iOS 26.0, *) func importCredentials(token: UUID) async throws -> ASExportedCredentialData { guard let data = try importCredentialsResult.get().data(using: .utf8) else { throw BitwardenError.dataError("importCredentialsResult data not set or not in UTF8") @@ -66,4 +74,7 @@ class MockCredentialImportManager: CredentialImportManager { ) } } + +struct MockCredentialExportManagerExportOptions: CredentialExportManagerExportOptions {} + #endif diff --git a/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableAccount+Extensions.swift b/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableAccount+Extensions.swift index 144d31962..2720566a0 100644 --- a/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableAccount+Extensions.swift +++ b/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableAccount+Extensions.swift @@ -2,7 +2,7 @@ import AuthenticationServices -@available(iOS 18.2, *) +@available(iOS 26.0, *) extension ASImportableAccount { // MARK: Static methods @@ -29,7 +29,7 @@ extension ASImportableAccount { /// Dumps the content of the `ASImportableAccount` into lines which can be used with /// inline snapshot assertion. - func dump() -> String { // swiftlint:disable:this cyclomatic_complexity function_body_length + func dump() -> String { // swiftlint:disable:this function_body_length var dumpResult = "" dumpResult.append("Email: \(email)\n") dumpResult.append("UserName: \(userName)\n") @@ -37,15 +37,14 @@ extension ASImportableAccount { let itemsResult = items.reduce(into: "") { result, item in result.appendWithIndentation("Title: \(item.title)\n") - result.appendWithIndentation("Type: \(item.type)\n") - result.appendWithIndentation("Creation: \(item.created)\n") - result.appendWithIndentation("Modified: \(item.lastModified)\n") + result.appendWithIndentation("Creation: \(String(describing: item.created))\n") + result.appendWithIndentation("Modified: \(String(describing: item.lastModified))\n") result.appendWithIndentation("--- Credentials ---\n") let credentialsResult = item.credentials.reduce(into: "") { credResult, credential in switch credential { case let .basicAuthentication(basicAuthentication): - if let username = basicAuthentication.username { + if let username = basicAuthentication.userName { credResult.appendWithIndentation("Username.FieldType: \(username.fieldType)\n", level: 2) credResult.appendWithIndentation("Username.Value: \(username.value)\n", level: 2) } @@ -53,16 +52,6 @@ extension ASImportableAccount { credResult.appendWithIndentation("Password.FieldType: \(password.fieldType)\n", level: 2) credResult.appendWithIndentation("Password.Value: \(password.value)\n", level: 2) } - if !basicAuthentication.urls.isEmpty { - credResult.appendWithIndentation("--- Urls ---\n", level: 2) - let urlsResult = basicAuthentication.urls.reduce(into: "") { urlResult, url in - urlResult.appendWithIndentation(url, level: 3) - if url != basicAuthentication.urls.last { - urlResult.appendWithIndentation("\n\n", level: 3) - } - } - credResult.appendWithIndentation(urlsResult, level: 2) - } case let .passkey(passkey): credResult.appendWithIndentation("CredentialID: \(passkey.credentialID)\n", level: 2) credResult.appendWithIndentation("Key: \(passkey.key)\n", level: 2) @@ -80,12 +69,12 @@ extension ASImportableAccount { } credResult.appendWithIndentation("Period: \(totp.period)\n", level: 2) credResult.appendWithIndentation("Secret: \(totp.secret)\n", level: 2) - credResult.appendWithIndentation("Username: \(totp.username)\n", level: 2) + credResult.appendWithIndentation("Username: \(totp.userName ?? "")\n", level: 2) case let .note(note): credResult.appendWithIndentation("Note: \(note.content)\n", level: 2) case let .creditCard(card): - credResult.appendWithIndentation("FullName: \(card.fullName)\n", level: 2) - credResult.appendWithIndentation("Number: \(card.number)\n", level: 2) + credResult.appendWithIndentation("FullName: \(String(describing: card.fullName))\n", level: 2) + credResult.appendWithIndentation("Number: \(String(describing: card.number))\n", level: 2) if let cardType = card.cardType { credResult.appendWithIndentation("CardType: \(cardType)\n", level: 2) } diff --git a/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableItem+Extensions.swift b/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableItem+Extensions.swift index 645e38cbe..faff0c613 100644 --- a/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableItem+Extensions.swift +++ b/BitwardenShared/Core/Vault/Services/TestHelpers/ASImportableItem+Extensions.swift @@ -1,16 +1,17 @@ #if SUPPORTS_CXP import AuthenticationServices -@available(iOS 18.2, *) +@available(iOS 26.0, *) extension ASImportableItem { /// Provides a fixture for `ASImportableItem`. static func fixture( id: Data = Data(capacity: 16), created: Date = .now, lastModified: Date = .now, - type: ASImportableItem.ItemType = .login, title: String = "", subtitle: String? = nil, + favorite: Bool = false, + scope: ASImportableCredentialScope? = nil, credentials: [ASImportableCredential] = [], tags: [String] = [] ) -> ASImportableItem { @@ -18,9 +19,10 @@ extension ASImportableItem { id: id, created: created, lastModified: lastModified, - type: type, title: title, subtitle: subtitle, + favorite: favorite, + scope: scope, credentials: credentials, tags: tags ) diff --git a/BitwardenShared/UI/Platform/Application/AppProcessor.swift b/BitwardenShared/UI/Platform/Application/AppProcessor.swift index 2265e184a..13809713b 100644 --- a/BitwardenShared/UI/Platform/Application/AppProcessor.swift +++ b/BitwardenShared/UI/Platform/Application/AppProcessor.swift @@ -235,7 +235,7 @@ public class AppProcessor { /// Handles importing credentials using Credential Exchange Protocol. /// - Parameter credentialImportToken: The credentials import token to user with the `ASCredentialImportManager`. - @available(iOSApplicationExtension 18.2, *) + @available(iOSApplicationExtension 26.0, *) public func handleImportCredentials(credentialImportToken: UUID) async { let route = AppRoute.tab(.vault(.importCXF( .importCredentials(credentialImportToken: credentialImportToken) diff --git a/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessor.swift b/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessor.swift index 780e76557..072a46708 100644 --- a/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessor.swift +++ b/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessor.swift @@ -102,7 +102,7 @@ class ExportCXFProcessor: StateProcessor ASPresentationAnchor + @MainActor + func presentationAnchorForASCredentialExportManager() async -> ASPresentationAnchor } diff --git a/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessorTests.swift b/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessorTests.swift index 95b930e7f..e5d1eeb5f 100644 --- a/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessorTests.swift +++ b/BitwardenShared/UI/Tools/ExportCXF/ExportCXF/ExportCXFProcessorTests.swift @@ -206,7 +206,7 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ CXFCredentialsResult(count: 10, type: .password), ]) - if #available(iOS 18.2, *) { + if #available(iOS 26.0, *) { exportCXFCiphersRepository.getExportVaultDataForCXFResult = .success( ASImportableAccount.fixture() @@ -216,7 +216,7 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ await subject.perform(.mainButtonTapped) // this should never happen in the actual app but here is a test for it as well. - guard #available(iOS 18.2, *) else { + guard #available(iOS 26.0, *) else { XCTAssertEqual(coordinator.alertShown.count, 1) XCTAssertEqual(coordinator.alertShown[0].title, Localizations.exportingFailed) return @@ -231,8 +231,8 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ /// `perform(_:)` with `.mainButtonTapped` in `.prepared` status does nothing when there's no delegate. @MainActor func test_perform_mainButtonTappedPreparedDoesNothingWhenDelegateNil() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } subject = ExportCXFProcessor( @@ -262,8 +262,8 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ /// `perform(_:)` with `.mainButtonTapped` in `.prepared` status throws when getting export data. @MainActor func test_perform_mainButtonTappedPreparedThrowsGettingExportData() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } subject.state.status = .prepared(itemsToExport: [ @@ -285,8 +285,8 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ /// `perform(_:)` with `.mainButtonTapped` in `.prepared` status throws when exporting credentials. @MainActor func test_perform_mainButtonTappedPreparedThrowsExportingCredentials() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } subject.state.status = .prepared(itemsToExport: [ @@ -311,8 +311,8 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ /// when exporting credentials. @MainActor func test_perform_mainButtonTappedPreparedThrowsAuthorizationExportingCredentials() async throws { - guard #available(iOS 18.2, *) else { - throw XCTSkip("This test requires iOS 18.2") + guard #available(iOS 26.0, *) else { + throw XCTSkip("This test requires iOS 26.0") } subject.state.status = .prepared(itemsToExport: [ @@ -339,7 +339,7 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ func test_perform_mainButtonTappedPreparedNothing() async throws { subject.state.status = .prepared(itemsToExport: []) await subject.perform(.mainButtonTapped) - throw XCTSkip("This feature is available on iOS 18.2 or later compiling with Xcode 16.2 or later") + throw XCTSkip("This feature is available on iOS 26.0 or later compiling with Xcode 26.0 or later") } #endif diff --git a/BitwardenShared/UI/Tools/ExportCXF/ExportCXFCoordinator.swift b/BitwardenShared/UI/Tools/ExportCXF/ExportCXFCoordinator.swift index 29174243a..fa9332332 100644 --- a/BitwardenShared/UI/Tools/ExportCXF/ExportCXFCoordinator.swift +++ b/BitwardenShared/UI/Tools/ExportCXF/ExportCXFCoordinator.swift @@ -80,7 +80,8 @@ extension ExportCXFCoordinator: HasErrorAlertServices { // MARK: - ExportCXFProcessorDelegate extension ExportCXFCoordinator: ExportCXFProcessorDelegate { - func presentationAnchorForASCredentialExportManager() -> ASPresentationAnchor { + @MainActor + func presentationAnchorForASCredentialExportManager() async -> ASPresentationAnchor { stackNavigator?.rootViewController?.view.window ?? UIWindow() } } diff --git a/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessor.swift b/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessor.swift index 3d1dc13ab..758df891c 100644 --- a/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessor.swift +++ b/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessor.swift @@ -66,7 +66,7 @@ class ImportCXFProcessor: StateProcessor /// Checks whether the CXF import feature is enabled. private func checkEnabled() async { - guard #available(iOS 18.2, *), await services.configService.getFeatureFlag(.cxpImportMobile) else { + guard #available(iOS 26.0, *), await services.configService.getFeatureFlag(.cxpImportMobile) else { state.status = .failure(message: Localizations.importingFromAnotherProviderIsNotAvailableForThisDevice) return } @@ -80,7 +80,7 @@ class ImportCXFProcessor: StateProcessor private func startImport() async { #if SUPPORTS_CXP - guard #available(iOS 18.2, *), let credentialImportToken = state.credentialImportToken else { + guard #available(iOS 26.0, *), let credentialImportToken = state.credentialImportToken else { coordinator.showAlert( .defaultAlert( title: Localizations.importError, diff --git a/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessorTests.swift b/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessorTests.swift index 00fbf43af..b2b99df49 100644 --- a/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessorTests.swift +++ b/BitwardenShared/UI/Tools/ImportCXF/ImportCXF/ImportCXFProcessorTests.swift @@ -76,7 +76,7 @@ class ImportCXFProcessorTests: BitwardenTestCase { /// policy applies to user. @MainActor func test_perform_appearedPersonalOwnership() async throws { - guard #available(iOS 18.2, *) else { + guard #available(iOS 26.0, *) else { throw XCTSkip("CXP Import feature is not available on this device") } @@ -98,7 +98,7 @@ class ImportCXFProcessorTests: BitwardenTestCase { /// policy doesn't apply to user. @MainActor func test_perform_appearedFeatureFlagEnabled() async throws { - guard #available(iOS 18.2, *) else { + guard #available(iOS 26.0, *) else { throw XCTSkip("CXP Import feature is not available on this device") } @@ -330,7 +330,7 @@ class ImportCXFProcessorTests: BitwardenTestCase { /// Checks whether the alert is shown when not in the correct iOS version for CXF Import to work. @MainActor private func checkAlertShownWhenNotInCorrectIOSVersion() -> Bool { - guard #available(iOS 18.2, *) else { + guard #available(iOS 26.0, *) else { XCTAssertEqual( coordinator.alertShown, [ diff --git a/Scripts/alpha_update_cxp_infoplist.sh b/Scripts/alpha_update_cxp_infoplist.sh index 0edaa9c0f..8e27b8cd4 100755 --- a/Scripts/alpha_update_cxp_infoplist.sh +++ b/Scripts/alpha_update_cxp_infoplist.sh @@ -15,6 +15,8 @@ app_info_plist_path="Bitwarden/Application/Support/Info.plist" if ! grep -q "SupportsCredentialExchange" $autofill_info_plist_path; then plutil -insert NSExtension.NSExtensionAttributes.ASCredentialProviderExtensionCapabilities.SupportsCredentialExchange -bool YES $autofill_info_plist_path + plutil -insert NSExtension.NSExtensionAttributes.ASCredentialProviderExtensionCapabilities.SupportedCredentialExchangeVersions -array $autofill_info_plist_path + plutil -insert NSExtension.NSExtensionAttributes.ASCredentialProviderExtensionCapabilities.SupportedCredentialExchangeVersions.0 -string "1.0" $autofill_info_plist_path fi if ! grep -q "ASCredentialExchangeActivityType" $app_info_plist_path; then