diff --git a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessor.swift b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessor.swift index 7abf123ee..289244875 100644 --- a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessor.swift +++ b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessor.swift @@ -11,6 +11,7 @@ class ImportCXPProcessor: StateProcessor typealias Services = HasConfigService & HasErrorReporter & HasImportCiphersRepository + & HasPolicyService & HasStateService // MARK: Private Properties @@ -68,6 +69,10 @@ class ImportCXPProcessor: StateProcessor state.status = .failure(message: Localizations.importingFromAnotherProviderIsNotAvailableForThisDevice) return } + if await services.policyService.policyAppliesToUser(.personalOwnership) { + state.isFeatureUnavailable = true + state.status = .failure(message: Localizations.personalOwnershipPolicyInEffect) + } } /// Starts the import process. @@ -110,7 +115,7 @@ class ImportCXPProcessor: StateProcessor /// Shows the alert confirming the user wants to import logins later. private func cancelWithConfirmation() { - guard !state.isFeatureUnvailable else { + guard !state.isFeatureUnavailable else { coordinator.navigate(to: .dismiss) return } diff --git a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessorTests.swift b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessorTests.swift index 82dd32b1f..b5631fdfb 100644 --- a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessorTests.swift +++ b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPProcessorTests.swift @@ -11,6 +11,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { var coordinator: MockCoordinator! var errorReporter: MockErrorReporter! var importCiphersRepository: MockImportCiphersRepository! + var policyService: MockPolicyService! var state: ImportCXPState! var stateService: MockStateService! var subject: ImportCXPProcessor! @@ -24,6 +25,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { coordinator = MockCoordinator() errorReporter = MockErrorReporter() importCiphersRepository = MockImportCiphersRepository() + policyService = MockPolicyService() state = ImportCXPState() stateService = MockStateService() subject = ImportCXPProcessor( @@ -32,6 +34,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { configService: configService, errorReporter: errorReporter, importCiphersRepository: importCiphersRepository, + policyService: policyService, stateService: stateService ), state: state @@ -45,6 +48,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { coordinator = nil errorReporter = nil importCiphersRepository = nil + policyService = nil state = nil stateService = nil subject = nil @@ -65,7 +69,30 @@ class ImportCXPProcessorTests: BitwardenTestCase { } /// `perform(_:)` with `.appeared` sets the status as `.failure` with a message - /// when the feature flag `.cxpImportMobile` is not enabled. + /// when the feature flag `.cxpImportMobile` is enabled. but `.personalOwnership` + /// policy applies to user. + @MainActor + func test_perform_appearedPersonalOwnership() async throws { + guard #available(iOS 18.2, *) else { + throw XCTSkip("CXP Import feature is not available on this device") + } + + configService.featureFlagsBool[.cxpImportMobile] = true + policyService.policyAppliesToUserResult[.personalOwnership] = true + + await subject.perform(.appeared) + + guard case let .failure(message) = subject.state.status else { + XCTFail("Status should be failure") + return + } + XCTAssertEqual(message, Localizations.personalOwnershipPolicyInEffect) + XCTAssertTrue(subject.state.isFeatureUnavailable) + } + + /// `perform(_:)` with `.appeared` doesn't set the status as `.failure` + /// when the feature flag `.cxpImportMobile` is enabled and `.personalOwnership` + /// policy doesn't apply to user. @MainActor func test_perform_appearedFeatureFlagEnabled() async throws { guard #available(iOS 18.2, *) else { @@ -82,7 +109,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { /// `perform(_:)` with `.cancel` with feature available shows confirmation and navigates to dismiss. @MainActor func test_perform_cancel() async throws { - subject.state.isFeatureUnvailable = false + subject.state.isFeatureUnavailable = false let task = Task { await subject.perform(.cancel) } @@ -108,7 +135,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { /// doesn't navigate to dismiss if the user cancels the confirmation dialog. @MainActor func test_perform_cancelNoConfirmation() async throws { - subject.state.isFeatureUnvailable = false + subject.state.isFeatureUnavailable = false let task = Task { await subject.perform(.cancel) } @@ -128,7 +155,7 @@ class ImportCXPProcessorTests: BitwardenTestCase { /// `perform(_:)` with `.cancel` with feature unavailable navigates to dismiss. @MainActor func test_perform_cancelFeatureUnavailable() async throws { - subject.state.isFeatureUnvailable = true + subject.state.isFeatureUnavailable = true let task = Task { await subject.perform(.cancel) } diff --git a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPState.swift b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPState.swift index 0c0a2e89b..8f8f48bf5 100644 --- a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPState.swift +++ b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPState.swift @@ -28,7 +28,7 @@ struct ImportCXPState: Equatable, Sendable { var credentialImportToken: UUID? /// Whether the CXP import feature is available. - var isFeatureUnvailable: Bool = false + var isFeatureUnavailable: Bool = false /// The title of the main button. var mainButtonTitle: String { @@ -83,7 +83,7 @@ struct ImportCXPState: Equatable, Sendable { case .success: Localizations.importSuccessful case .failure: - isFeatureUnvailable ? Localizations.importNotAvailable : Localizations.importFailed + isFeatureUnavailable ? Localizations.importNotAvailable : Localizations.importFailed } } @@ -99,7 +99,7 @@ struct ImportCXPState: Equatable, Sendable { /// Whether to show the main button. var showMainButton: Bool { - status != .importing || isFeatureUnvailable + status != .importing && !isFeatureUnavailable } /// The current status of the import process. diff --git a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPStateTests.swift b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPStateTests.swift index 47a342ba9..5838a63f2 100644 --- a/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPStateTests.swift +++ b/BitwardenShared/UI/Tools/ImportCXP/ImportCXP/ImportCXPStateTests.swift @@ -84,7 +84,7 @@ class ImportCXPStateTests: BitwardenTestCase { subject.status = .failure(message: "Something went wrong") XCTAssertEqual(subject.title, Localizations.importFailed) - subject.isFeatureUnvailable = true + subject.isFeatureUnavailable = true XCTAssertEqual(subject.title, Localizations.importNotAvailable) } @@ -117,4 +117,20 @@ class ImportCXPStateTests: BitwardenTestCase { subject.status = .failure(message: "Something went wrong") XCTAssertTrue(subject.showMainButton) } + + /// `getter:showMainButton` returns `false` when feature unavailable. + func test_showMainButton_featureUnavailable() { + subject.isFeatureUnavailable = true + subject.status = .start + XCTAssertFalse(subject.showMainButton) + + subject.status = .importing + XCTAssertFalse(subject.showMainButton) + + subject.status = .success(totalImportedCredentials: 1, importedResults: []) + XCTAssertFalse(subject.showMainButton) + + subject.status = .failure(message: "Something went wrong") + XCTAssertFalse(subject.showMainButton) + } } diff --git a/BitwardenShared/UI/Tools/ImportCXP/ImportCXPCoordinator.swift b/BitwardenShared/UI/Tools/ImportCXP/ImportCXPCoordinator.swift index 2b7443461..de5fa8291 100644 --- a/BitwardenShared/UI/Tools/ImportCXP/ImportCXPCoordinator.swift +++ b/BitwardenShared/UI/Tools/ImportCXP/ImportCXPCoordinator.swift @@ -8,6 +8,7 @@ class ImportCXPCoordinator: Coordinator, HasStackNavigator { typealias Services = HasConfigService & HasErrorReporter & HasImportCiphersRepository + & HasPolicyService & HasStateService // MARK: Private Properties