mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 04:34:55 -06:00
[PM-22549] Update CXP related code to work with new iOS 26 beta API (#1656)
This commit is contained in:
parent
cfc401f6e0
commit
64bbab85db
@ -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:
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
[
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user