Skip to content

Commit

Permalink
[PM-17467] Wrapped Credential Exchange related APIs into SUPPORTS_CXP…
Browse files Browse the repository at this point in the history
… compiler flag (#1295)
  • Loading branch information
fedemkr authored Jan 23, 2025
1 parent bb1c48b commit e7ea8b1
Show file tree
Hide file tree
Showing 20 changed files with 159 additions and 1 deletion.
12 changes: 12 additions & 0 deletions Bitwarden/Application/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
appProcessor.handleAppLinks(incomingURL: incomingURL)
}

#if SUPPORTS_CXP

if #available(iOS 18.2, *),
let userActivity = connectionOptions.userActivities.first {
await checkAndHandleCredentialExchangeActivity(appProcessor: appProcessor, userActivity: userActivity)
}

#endif
}
}

Expand All @@ -93,11 +97,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
appProcessor.handleAppLinks(incomingURL: incomingURL)
}

#if SUPPORTS_CXP

if #available(iOS 18.2, *) {
Task {
await checkAndHandleCredentialExchangeActivity(appProcessor: appProcessor, userActivity: userActivity)
}
}

#endif
}

func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
Expand Down Expand Up @@ -174,6 +182,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

// MARK: - SceneDelegate 18.2

#if SUPPORTS_CXP

@available(iOS 18.2, *)
extension SceneDelegate {
/// Checks whether there is an `ASCredentialExchangeActivity` in the `userActivity` and handles it.
Expand All @@ -192,3 +202,5 @@ extension SceneDelegate {
await appProcessor.handleImportCredentials(credentialImportToken: token)
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,26 @@ protocol ExportCXFCiphersRepository {
/// - Returns: An array of `CXFCredentialsResult` that has the summary of the ciphers to export by type.
func buildCiphersToExportSummary(from ciphers: [Cipher]) -> [CXFCredentialsResult]

#if SUPPORTS_CXP
/// 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
#endif

/// Gets all ciphers to export in Credential Exchange flow.
///
/// - Returns: Ciphers to export.
func getAllCiphersToExportCXF() async throws -> [Cipher]

#if SUPPORTS_CXP
/// Exports the vault creating the `ASImportableAccount` to be used in Credential Exchange Protocol.
///
/// - Returns: An `ASImportableAccount`
@available(iOS 18.2, *)
func getExportVaultDataForCXF() async throws -> ASImportableAccount
#endif
}

class DefaultExportCXFCiphersRepository: ExportCXFCiphersRepository {
Expand Down Expand Up @@ -86,17 +90,23 @@ class DefaultExportCXFCiphersRepository: ExportCXFCiphersRepository {
return cxfCredentialsResultBuilder.build(from: ciphers).filter { !$0.isEmpty }
}

#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]))
}

#endif

func getAllCiphersToExportCXF() async throws -> [Cipher] {
try await cipherService.fetchAllCiphers()
.filter { $0.deletedDate == nil }
}

#if SUPPORTS_CXP

@available(iOS 18.2, *)
func getExportVaultDataForCXF() async throws -> ASImportableAccount {
let ciphers = try await getAllCiphersToExportCXF()
Expand All @@ -110,4 +120,6 @@ class DefaultExportCXFCiphersRepository: ExportCXFCiphersRepository {
let serializedCXF = try await clientService.exporters().exportCxf(account: sdkAccount, ciphers: ciphers)
return try JSONDecoder.cxfDecoder.decode(ASImportableAccount.self, from: Data(serializedCXF.utf8))
}

#endif
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if SUPPORTS_CXP
import AuthenticationServices
import InlineSnapshotTesting
import XCTest
Expand Down Expand Up @@ -236,3 +237,5 @@ class ExportCXFCiphersRepositoryTests: BitwardenTestCase {
}
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ extension DefaultImportCiphersRepository: ImportCiphersRepository {
credentialImportToken: UUID,
onProgress: @MainActor (Double) -> Void
) async throws -> [CXFCredentialsResult] {
#if SUPPORTS_CXP

let credentialData = try await credentialManagerFactory.createImportManager().importCredentials(
token: credentialImportToken
)
Expand Down Expand Up @@ -108,6 +110,9 @@ extension DefaultImportCiphersRepository: ImportCiphersRepository {
await onProgress(1.0)

return importedCredentialsCount.filter { !$0.isEmpty }
#else
return []
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if SUPPORTS_CXP
import AuthenticationServices
import XCTest

Expand Down Expand Up @@ -252,3 +253,4 @@ class ImportCiphersRepositoryTests: BitwardenTestCase {
return credentialDataJsonString
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import BitwardenSdk

class MockExportCXFCiphersRepository: ExportCXFCiphersRepository {
var buildCiphersToExportSummaryResult: [CXFCredentialsResult] = []
#if SUPPORTS_CXP
var exportCredentialsData: ImportableAccountProxy?
var exportCredentialsError: Error?
#endif
var getAllCiphersToExportCXFResult: Result<[Cipher], Error> = .failure(BitwardenTestError.example)
#if SUPPORTS_CXP
var getExportVaultDataForCXFResult: Result<ImportableAccountProxy, Error> = .failure(BitwardenTestError.example)
#endif

func buildCiphersToExportSummary(from ciphers: [Cipher]) -> [CXFCredentialsResult] {
buildCiphersToExportSummaryResult
}

#if SUPPORTS_CXP

@available(iOS 18.2, *)
func exportCredentials(data: ASImportableAccount, presentationAnchor: () -> ASPresentationAnchor) async throws {
exportCredentialsData = data
Expand All @@ -22,23 +28,31 @@ class MockExportCXFCiphersRepository: ExportCXFCiphersRepository {
}
}

#endif

func getAllCiphersToExportCXF() async throws -> [Cipher] {
try getAllCiphersToExportCXFResult.get()
}

#if SUPPORTS_CXP

@available(iOS 18.2, *)
func getExportVaultDataForCXF() async throws -> ASImportableAccount {
guard let result = try getExportVaultDataForCXFResult.get() as? ASImportableAccount else {
throw MockExportCXFCiphersRepositoryError.unableToCastToASImportableAccount
}
return result
}

#endif
}

protocol ImportableAccountProxy {}

#if SUPPORTS_CXP
@available(iOS 18.2, *)
extension ASImportableAccount: ImportableAccountProxy {}
#endif

enum MockExportCXFCiphersRepositoryError: Error {
case unableToCastToASImportableAccount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,28 @@ protocol CredentialImportManager: AnyObject {
// This section is needed for compiling the project on Xcode version < 16.2
// and to ease unit testing.

#if SUPPORTS_CXP

@available(iOS 18.2, *)
extension ASCredentialExportManager: CredentialExportManager {}

@available(iOS 18.2, *)
extension ASCredentialImportManager: CredentialImportManager {}

#else

class ASCredentialImportManager: CredentialImportManager {
func importCredentials(token: UUID) async throws -> ASExportedCredentialData {
ASExportedCredentialData()
}
}

class ASCredentialExportManager: CredentialExportManager {
init(presentationAnchor: ASPresentationAnchor) {}

func exportCredentials(_ credentialData: ASExportedCredentialData) async throws {}
}

struct ASExportedCredentialData {}

#endif
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#if SUPPORTS_CXP
import AuthenticationServices
#endif
import XCTest

@testable import BitwardenShared
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if SUPPORTS_CXP
import AuthenticationServices
import BitwardenSdk

Expand Down Expand Up @@ -64,3 +65,4 @@ class MockCredentialImportManager: CredentialImportManager {
)
}
}
#endif
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#if SUPPORTS_CXP

import AuthenticationServices

@available(iOS 18.2, *)
Expand Down Expand Up @@ -119,3 +121,5 @@ private extension String {
append("\(indentation)\(other)")
}
}

#endif
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#if SUPPORTS_CXP
import AuthenticationServices

@available(iOS 18.2, *)
Expand Down Expand Up @@ -25,3 +26,4 @@ extension ASImportableItem {
)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,13 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator { // swiftlint:d
///
@MainActor
private func showExportVault() async {
guard await services.configService.getFeatureFlag(.cxpExportMobile) else {
#if SUPPORTS_CXP
let cxpEnabled = true
#else
let cxpEnabled = false
#endif

guard cxpEnabled, await services.configService.getFeatureFlag(.cxpExportMobile) else {
navigate(to: .exportVaultToFile)
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ class SettingsCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this ty
}
defer { task.cancel() }

#if SUPPORTS_CXP

try await waitForAsync { [weak self] in
guard let self else { return true }
return stackNavigator.actions.last != nil
Expand All @@ -209,6 +211,19 @@ class SettingsCoordinatorTests: BitwardenTestCase { // swiftlint:disable:this ty
let action = try XCTUnwrap(stackNavigator.actions.last)
XCTAssertEqual(action.type, .pushed)
XCTAssertTrue(action.view is UIHostingController<ExportSettingsView>)

#else

try await waitForAsync { [weak self] in
guard let self else { return true }
return stackNavigator.actions.last?.view is UINavigationController
}

let navigationController = try XCTUnwrap(stackNavigator.actions.last?.view as? UINavigationController)
XCTAssertTrue(stackNavigator.actions.last?.view is UINavigationController)
XCTAssertTrue(navigationController.viewControllers.first is UIHostingController<ExportVaultView>)

#endif
}

/// `navigate(to:)` with `.exportVaultToFile` presents the export vault to file view.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class ExportCXFProcessor: StateProcessor<ExportCXFState, ExportCXFAction, Export

/// Starts the export process.
private func startExport() async {
#if SUPPORTS_CXP

guard #available(iOS 18.2, *) else {
coordinator.showAlert(
.defaultAlert(
Expand Down Expand Up @@ -135,6 +137,8 @@ class ExportCXFProcessor: StateProcessor<ExportCXFState, ExportCXFAction, Export
state.status = .failure(message: Localizations.thereHasBeenAnIssueExportingItems)
services.errorReporter.log(error: error)
}

#endif
}

/// Shows the alert confirming the user wants to export items later.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
try await preparesExportZeroItemsFromStatusTest(status: .failure(message: "failure"), fromAppeared: true)
}

#if SUPPORTS_CXP

/// `perform(_:)` with `.mainButtonTapped` in `.prepared` status starts export.
@MainActor
func test_perform_mainButtonTappedPreparedStartsExport() async throws {
Expand Down Expand Up @@ -327,6 +329,18 @@ class ExportCXFProcessorTests: BitwardenTestCase { // swiftlint:disable:this typ
)
}

#else

/// `perform(_:)` with `.mainButtonTapped` in `.prepared` status does nothing.
@MainActor
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")
}

#endif

// MARK: Private

/// Prepares export in the given status.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ class ImportCXFProcessor: StateProcessor<ImportCXFState, Void, ImportCXFEffect>

/// Starts the import process.
private func startImport() async {
#if SUPPORTS_CXP

guard #available(iOS 18.2, *), let credentialImportToken = state.credentialImportToken else {
coordinator.showAlert(
.defaultAlert(
Expand Down Expand Up @@ -107,6 +109,8 @@ class ImportCXFProcessor: StateProcessor<ImportCXFState, Void, ImportCXFEffect>
state.status = .failure(message: Localizations.thereWasAnIssueImportingAllOfYourPasswordsNoDataWasDeleted)
services.errorReporter.log(error: error)
}

#endif
}

/// Shows the alert confirming the user wants to import logins later.
Expand Down
Loading

0 comments on commit e7ea8b1

Please sign in to comment.