[PM-22549] Update CXP related code to work with new iOS 26 beta API (#1656)

This commit is contained in:
Federico Maccaroni 2025-08-07 16:35:09 -03:00 committed by GitHub
parent cfc401f6e0
commit 64bbab85db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 181 additions and 205 deletions

View File

@ -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:

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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"),

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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 {}

View File

@ -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()

View File

@ -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<CredentialExportManagerExportOptions, Error> = .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<String, Error> = .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

View File

@ -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)
}

View File

@ -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
)

View File

@ -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)

View File

@ -102,7 +102,7 @@ class ExportCXFProcessor: StateProcessor<ExportCXFState, ExportCXFAction, Export
private func startExport() async {
#if SUPPORTS_CXP
guard #available(iOS 18.2, *) else {
guard #available(iOS 26.0, *) else {
coordinator.showAlert(
.defaultAlert(
title: Localizations.exportingFailed
@ -123,7 +123,7 @@ class ExportCXFProcessor: StateProcessor<ExportCXFState, ExportCXFAction, Export
coordinator.hideLoadingOverlay()
try await services.exportCXFCiphersRepository.exportCredentials(
data: data,
presentationAnchor: { delegate.presentationAnchorForASCredentialExportManager() }
presentationAnchor: { await delegate.presentationAnchorForASCredentialExportManager() }
)
coordinator.navigate(to: .dismiss)
} catch ASAuthorizationError.failed {
@ -162,5 +162,6 @@ class ExportCXFProcessor: StateProcessor<ExportCXFState, ExportCXFAction, Export
protocol ExportCXFProcessorDelegate: AnyObject {
/// Returns an `ASPresentationAnchor` to be used when creating an `ASCredentialExportManager`.
/// - Returns: An `ASPresentationAnchor`.
func presentationAnchorForASCredentialExportManager() -> ASPresentationAnchor
@MainActor
func presentationAnchorForASCredentialExportManager() async -> ASPresentationAnchor
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -66,7 +66,7 @@ class ImportCXFProcessor: StateProcessor<ImportCXFState, Void, ImportCXFEffect>
/// 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<ImportCXFState, Void, ImportCXFEffect>
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,

View File

@ -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,
[

View File

@ -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