From 73d9e0572997f0eb6f3af30038f1eccb55291141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Wed, 24 Jun 2026 23:20:15 +0100 Subject: [PATCH 1/6] Disable Credential Exchange on F-Droid builds --- .../feature/importitems/ImportItemsScreen.kt | 29 +++++++++++++------ .../importitems/ImportItemsScreenTest.kt | 28 ++++++++++++++++-- cxf/build.gradle.kts | 18 +++++++++++- .../CredentialExchangeImporterImpl.kt | 26 +++++++++++++++++ .../CredentialExchangeRegistryImpl.kt | 29 +++++++++++++++++++ .../importer/CredentialExchangeImporter.kt | 7 +++++ .../CredentialExchangeImporterImpl.kt | 2 ++ .../CredentialExchangeRegistryImpl.kt | 0 .../CredentialExchangeImporterTest.kt | 28 ++++++++++++++++++ .../CredentialExchangeImporterTest.kt | 5 ++++ 10 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt create mode 100644 cxf/src/fdroid/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt rename cxf/src/{main => standard}/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt (98%) rename cxf/src/{main => standard}/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt (100%) create mode 100644 cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt rename cxf/src/{test => testStandard}/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt (97%) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt index 734134c647d..06fbd279b0d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt @@ -98,6 +98,7 @@ fun ImportItemsScreen( onNavigateBack = handler.onNavigateBack, onImportFromComputerClick = handler.onImportFromComputerClick, onImportFromAnotherAppClick = handler.onImportFromAnotherAppClick, + isImportFromAnotherAppSupported = credentialExchangeImporter.isSupported(), snackbarHostState = snackbarHostState, ) } @@ -108,6 +109,7 @@ private fun ImportItemsScaffold( onNavigateBack: () -> Unit, onImportFromComputerClick: () -> Unit, onImportFromAnotherAppClick: () -> Unit, + isImportFromAnotherAppSupported: Boolean, modifier: Modifier = Modifier, snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(), ) { @@ -136,6 +138,7 @@ private fun ImportItemsScaffold( ImportItemsContent( onImportFromComputerClick = onImportFromComputerClick, onImportFromAnotherAppClick = onImportFromAnotherAppClick, + isImportFromAnotherAppSupported = isImportFromAnotherAppSupported, modifier = Modifier .fillMaxSize() .standardHorizontalMargin(), @@ -147,6 +150,7 @@ private fun ImportItemsScaffold( private fun ImportItemsContent( onImportFromComputerClick: () -> Unit, onImportFromAnotherAppClick: () -> Unit, + isImportFromAnotherAppSupported: Boolean, modifier: Modifier = Modifier, ) { LazyColumn( @@ -159,19 +163,25 @@ private fun ImportItemsContent( BitwardenTextRow( text = stringResource(BitwardenString.import_from_computer), onClick = onImportFromComputerClick, - cardStyle = CardStyle.Top(), + cardStyle = if (isImportFromAnotherAppSupported) { + CardStyle.Top() + } else { + CardStyle.Full + }, modifier = Modifier.fillMaxWidth(), ) } - item { - BitwardenTextRow( - text = stringResource(BitwardenString.import_from_another_app), - onClick = onImportFromAnotherAppClick, - isExternalLink = true, - cardStyle = CardStyle.Bottom, - modifier = Modifier.fillMaxWidth(), - ) + if (isImportFromAnotherAppSupported) { + item { + BitwardenTextRow( + text = stringResource(BitwardenString.import_from_another_app), + onClick = onImportFromAnotherAppClick, + isExternalLink = true, + cardStyle = CardStyle.Bottom, + modifier = Modifier.fillMaxWidth(), + ) + } } item { Spacer(Modifier.height(16.dp)) } @@ -212,6 +222,7 @@ private fun ImportItemsContent_preview() { onNavigateBack = {}, onImportFromComputerClick = {}, onImportFromAnotherAppClick = {}, + isImportFromAnotherAppSupported = true, ) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt index 4440109b00e..8497028abd7 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt @@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test class ImportItemsScreenTest : BitwardenComposeTest() { @@ -43,8 +42,8 @@ class ImportItemsScreenTest : BitwardenComposeTest() { every { trySendAction(any()) } just runs } - @Before - fun setUp() { + private fun setUpContent(isImportSupported: Boolean = true) { + every { credentialExchangeImporter.isSupported() } returns isImportSupported setContent( credentialExchangeImporter = credentialExchangeImporter, ) { @@ -58,6 +57,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `initial state should be correct`() = runTest { + setUpContent() assertEquals( DEFAULT_STATE, viewModel.stateFlow.value, @@ -72,14 +72,29 @@ class ImportItemsScreenTest : BitwardenComposeTest() { .assertExists() } + @Test + fun `import from another app should not display when import is not supported`() = runTest { + setUpContent(isImportSupported = false) + + composeTestRule + .onNodeWithText("Import from computer") + .assertExists() + + composeTestRule + .onNodeWithText("Import from another app") + .assertDoesNotExist() + } + @Test fun `NavigateBack should call onNavigateBack`() { + setUpContent() mockEventFlow.tryEmit(ImportItemsEvent.NavigateBack) assertTrue(onNavigateBackCalled) } @Test fun `onBackClick should send BackClick`() { + setUpContent() composeTestRule.onNodeWithContentDescription("Back").performClick() verify(exactly = 1) { viewModel.trySendAction(ImportItemsAction.BackClick) @@ -89,6 +104,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `ImportFromComputer click should send NavigateToImportFromComputer action`() = runTest { + setUpContent() composeTestRule .onNodeWithText("Import from computer") .performClick() @@ -100,6 +116,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `NavigateToImportFromComputer should call onNavigateToImportFromComputer`() { + setUpContent() mockEventFlow.tryEmit(ImportItemsEvent.NavigateToImportFromComputer) assertTrue(onNavigateToImportFromComputerCalled) } @@ -107,6 +124,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `ImportFromAnotherApp click should send NavigateToImportFromAnotherApp action`() = runTest { + setUpContent() composeTestRule .onNodeWithText("Import from another app") .performClick() @@ -117,6 +135,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `ShowRegisteredImportSources should call CredentialExchangeImporter`() = runTest { + setUpContent() val importCredentialsSelectionResult = ImportCredentialsSelectionResult.Success( response = "mockResponse", callingAppInfo = mockk(relaxed = true), @@ -141,6 +160,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `General dialog should display based on state`() = runTest { + setUpContent() mockkStateFlow.tryEmit(ImportItemsState()) composeTestRule @@ -164,6 +184,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `General dialog dismiss should send DismissDialog`() = runTest { + setUpContent() mockkStateFlow.tryEmit( ImportItemsState( dialog = ImportItemsState.DialogState.General( @@ -185,6 +206,7 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `BitwardenLoadingDialog should display based on state`() = runTest { + setUpContent() mockkStateFlow.tryEmit( ImportItemsState( dialog = ImportItemsState.DialogState.Loading(message = "message".asText()), diff --git a/cxf/build.gradle.kts b/cxf/build.gradle.kts index 9b888b572d3..cfa89ad7207 100644 --- a/cxf/build.gradle.kts +++ b/cxf/build.gradle.kts @@ -33,6 +33,16 @@ configure { ) } } + flavorDimensions += listOf("mode") + productFlavors { + create("standard") { + isDefault = true + dimension = "mode" + } + create("fdroid") { + dimension = "mode" + } + } compileOptions { sourceCompatibility(libs.versions.jvmTarget.get()) targetCompatibility(libs.versions.jvmTarget.get()) @@ -46,6 +56,9 @@ kotlin { } dependencies { + fun standardImplementation(dependencyNotation: Any) { + add("standardImplementation", dependencyNotation) + } implementation(project(":annotation")) implementation(project(":core")) @@ -57,13 +70,16 @@ dependencies { implementation(libs.androidx.compose.runtime) implementation(libs.androidx.credentials) implementation(libs.androidx.credentials.providerevents) - implementation(libs.androidx.credentials.providerevents.play.services) implementation(libs.google.hilt.android) ksp(libs.google.hilt.compiler) implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.serialization) implementation(libs.timber) + // Play Services backend for ProviderEventsManager is not FOSS, so it is excluded from the + // F-Droid flavor. The F-Droid flavor supplies no-op stubs instead. + standardImplementation(libs.androidx.credentials.providerevents.play.services) + testImplementation(platform(libs.junit.bom)) testRuntimeOnly(libs.junit.platform.launcher) testImplementation(libs.junit.jupiter) diff --git a/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt b/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt new file mode 100644 index 00000000000..e8e3cc4dbff --- /dev/null +++ b/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt @@ -0,0 +1,26 @@ +package com.bitwarden.cxf.importer + +import android.content.Context +import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException +import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult + +/** + * F-Droid implementation of [CredentialExchangeImporter]. Credential exchange relies on the + * Play Services backend, which is not available on F-Droid builds, so import is unsupported and + * always reports a failure if invoked. + */ +@OmitFromCoverage +@Suppress("UnusedParameter") +internal class CredentialExchangeImporterImpl( + activity: Context, +) : CredentialExchangeImporter { + + override fun isSupported(): Boolean = false + + override suspend fun importCredentials( + credentialTypes: List, + ): ImportCredentialsSelectionResult = ImportCredentialsSelectionResult.Failure( + error = ImportCredentialsUnknownErrorException(), + ) +} diff --git a/cxf/src/fdroid/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt b/cxf/src/fdroid/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt new file mode 100644 index 00000000000..249a862e945 --- /dev/null +++ b/cxf/src/fdroid/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt @@ -0,0 +1,29 @@ +package com.bitwarden.cxf.registry + +import android.app.Application +import androidx.credentials.providerevents.transfer.ClearExportResponse +import androidx.credentials.providerevents.transfer.RegisterExportResponse +import com.bitwarden.annotation.OmitFromCoverage +import com.bitwarden.core.data.util.asFailure +import com.bitwarden.cxf.registry.model.RegistrationRequest + +/** + * F-Droid implementation of [CredentialExchangeRegistry]. Registration relies on the Play Services + * backend, which is not available on F-Droid builds, so registration is a no-op that always fails. + */ +@OmitFromCoverage +@Suppress("UnusedParameter") +internal class CredentialExchangeRegistryImpl( + application: Application, +) : CredentialExchangeRegistry { + + override suspend fun register( + registrationRequest: RegistrationRequest, + ): Result = + UnsupportedOperationException("Credential exchange is not supported on F-Droid builds.") + .asFailure() + + override suspend fun unregister(): Result = + UnsupportedOperationException("Credential exchange is not supported on F-Droid builds.") + .asFailure() +} diff --git a/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt b/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt index ee3890b67a0..a9affa09334 100644 --- a/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt +++ b/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt @@ -6,6 +6,13 @@ import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult * Responsible for importing credentials from other apps. */ interface CredentialExchangeImporter { + /** + * Returns `true` if credential exchange import is available on the current build, or `false` + * if it is unsupported (e.g. on builds without the Credential Exchange backend). Callers + * should hide or disable import entry points when this returns `false`. + */ + fun isSupported(): Boolean + /** * Starts the import process by requesting selection of a source credential provider. * diff --git a/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt b/cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt similarity index 98% rename from cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt rename to cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt index 0a08cf43af5..04cecf8f1aa 100644 --- a/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt +++ b/cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt @@ -24,6 +24,8 @@ internal class CredentialExchangeImporterImpl( ProviderEventsManager.create(activity), ) : CredentialExchangeImporter { + override fun isSupported(): Boolean = true + override suspend fun importCredentials( credentialTypes: List, ): ImportCredentialsSelectionResult = try { diff --git a/cxf/src/main/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt b/cxf/src/standard/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt similarity index 100% rename from cxf/src/main/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt rename to cxf/src/standard/kotlin/com/bitwarden/cxf/registry/CredentialExchangeRegistryImpl.kt diff --git a/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt b/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt new file mode 100644 index 00000000000..d8db8918beb --- /dev/null +++ b/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt @@ -0,0 +1,28 @@ +package com.bitwarden.cxf.importer + +import android.app.Activity +import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CredentialExchangeImporterTest { + + private val importer = CredentialExchangeImporterImpl( + activity = mockk(relaxed = true), + ) + + @Test + fun `isSupported should return false`() { + assertFalse(importer.isSupported()) + } + + @Test + fun `importCredentials should return Failure`() = runTest { + val result = importer.importCredentials(listOf("basic-auth")) + + assertTrue(result is ImportCredentialsSelectionResult.Failure) + } +} diff --git a/cxf/src/test/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt b/cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt similarity index 97% rename from cxf/src/test/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt rename to cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt index 90038f726c9..755f7eb867f 100644 --- a/cxf/src/test/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt +++ b/cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt @@ -57,6 +57,11 @@ class CredentialExchangeImporterTest { ) } + @Test + fun `isSupported should return true`() { + assertTrue(importer.isSupported()) + } + @Test fun `importCredentials should return Success when provider returns valid response`() = runTest { From 710cb4473bea387ca3a92684733e55145b246295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Thu, 9 Jul 2026 19:23:24 +0100 Subject: [PATCH 2/6] Revert ImportItemsScreen changes --- .../feature/importitems/ImportItemsScreen.kt | 29 ++++++------------- .../importitems/ImportItemsScreenTest.kt | 28 ++---------------- 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt index 06fbd279b0d..734134c647d 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreen.kt @@ -98,7 +98,6 @@ fun ImportItemsScreen( onNavigateBack = handler.onNavigateBack, onImportFromComputerClick = handler.onImportFromComputerClick, onImportFromAnotherAppClick = handler.onImportFromAnotherAppClick, - isImportFromAnotherAppSupported = credentialExchangeImporter.isSupported(), snackbarHostState = snackbarHostState, ) } @@ -109,7 +108,6 @@ private fun ImportItemsScaffold( onNavigateBack: () -> Unit, onImportFromComputerClick: () -> Unit, onImportFromAnotherAppClick: () -> Unit, - isImportFromAnotherAppSupported: Boolean, modifier: Modifier = Modifier, snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(), ) { @@ -138,7 +136,6 @@ private fun ImportItemsScaffold( ImportItemsContent( onImportFromComputerClick = onImportFromComputerClick, onImportFromAnotherAppClick = onImportFromAnotherAppClick, - isImportFromAnotherAppSupported = isImportFromAnotherAppSupported, modifier = Modifier .fillMaxSize() .standardHorizontalMargin(), @@ -150,7 +147,6 @@ private fun ImportItemsScaffold( private fun ImportItemsContent( onImportFromComputerClick: () -> Unit, onImportFromAnotherAppClick: () -> Unit, - isImportFromAnotherAppSupported: Boolean, modifier: Modifier = Modifier, ) { LazyColumn( @@ -163,25 +159,19 @@ private fun ImportItemsContent( BitwardenTextRow( text = stringResource(BitwardenString.import_from_computer), onClick = onImportFromComputerClick, - cardStyle = if (isImportFromAnotherAppSupported) { - CardStyle.Top() - } else { - CardStyle.Full - }, + cardStyle = CardStyle.Top(), modifier = Modifier.fillMaxWidth(), ) } - if (isImportFromAnotherAppSupported) { - item { - BitwardenTextRow( - text = stringResource(BitwardenString.import_from_another_app), - onClick = onImportFromAnotherAppClick, - isExternalLink = true, - cardStyle = CardStyle.Bottom, - modifier = Modifier.fillMaxWidth(), - ) - } + item { + BitwardenTextRow( + text = stringResource(BitwardenString.import_from_another_app), + onClick = onImportFromAnotherAppClick, + isExternalLink = true, + cardStyle = CardStyle.Bottom, + modifier = Modifier.fillMaxWidth(), + ) } item { Spacer(Modifier.height(16.dp)) } @@ -222,7 +212,6 @@ private fun ImportItemsContent_preview() { onNavigateBack = {}, onImportFromComputerClick = {}, onImportFromAnotherAppClick = {}, - isImportFromAnotherAppSupported = true, ) } } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt index 8497028abd7..4440109b00e 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/importitems/ImportItemsScreenTest.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test class ImportItemsScreenTest : BitwardenComposeTest() { @@ -42,8 +43,8 @@ class ImportItemsScreenTest : BitwardenComposeTest() { every { trySendAction(any()) } just runs } - private fun setUpContent(isImportSupported: Boolean = true) { - every { credentialExchangeImporter.isSupported() } returns isImportSupported + @Before + fun setUp() { setContent( credentialExchangeImporter = credentialExchangeImporter, ) { @@ -57,7 +58,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `initial state should be correct`() = runTest { - setUpContent() assertEquals( DEFAULT_STATE, viewModel.stateFlow.value, @@ -72,29 +72,14 @@ class ImportItemsScreenTest : BitwardenComposeTest() { .assertExists() } - @Test - fun `import from another app should not display when import is not supported`() = runTest { - setUpContent(isImportSupported = false) - - composeTestRule - .onNodeWithText("Import from computer") - .assertExists() - - composeTestRule - .onNodeWithText("Import from another app") - .assertDoesNotExist() - } - @Test fun `NavigateBack should call onNavigateBack`() { - setUpContent() mockEventFlow.tryEmit(ImportItemsEvent.NavigateBack) assertTrue(onNavigateBackCalled) } @Test fun `onBackClick should send BackClick`() { - setUpContent() composeTestRule.onNodeWithContentDescription("Back").performClick() verify(exactly = 1) { viewModel.trySendAction(ImportItemsAction.BackClick) @@ -104,7 +89,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `ImportFromComputer click should send NavigateToImportFromComputer action`() = runTest { - setUpContent() composeTestRule .onNodeWithText("Import from computer") .performClick() @@ -116,7 +100,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `NavigateToImportFromComputer should call onNavigateToImportFromComputer`() { - setUpContent() mockEventFlow.tryEmit(ImportItemsEvent.NavigateToImportFromComputer) assertTrue(onNavigateToImportFromComputerCalled) } @@ -124,7 +107,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `ImportFromAnotherApp click should send NavigateToImportFromAnotherApp action`() = runTest { - setUpContent() composeTestRule .onNodeWithText("Import from another app") .performClick() @@ -135,7 +117,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `ShowRegisteredImportSources should call CredentialExchangeImporter`() = runTest { - setUpContent() val importCredentialsSelectionResult = ImportCredentialsSelectionResult.Success( response = "mockResponse", callingAppInfo = mockk(relaxed = true), @@ -160,7 +141,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `General dialog should display based on state`() = runTest { - setUpContent() mockkStateFlow.tryEmit(ImportItemsState()) composeTestRule @@ -184,7 +164,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `General dialog dismiss should send DismissDialog`() = runTest { - setUpContent() mockkStateFlow.tryEmit( ImportItemsState( dialog = ImportItemsState.DialogState.General( @@ -206,7 +185,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() { @Test fun `BitwardenLoadingDialog should display based on state`() = runTest { - setUpContent() mockkStateFlow.tryEmit( ImportItemsState( dialog = ImportItemsState.DialogState.Loading(message = "message".asText()), From c8584ac4a9da04daa8e94c77e80fe67aa5012013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Thu, 9 Jul 2026 21:05:17 +0100 Subject: [PATCH 3/6] Revert isSupported --- .../cxf/importer/CredentialExchangeImporterImpl.kt | 2 -- .../bitwarden/cxf/importer/CredentialExchangeImporter.kt | 7 ------- .../cxf/importer/CredentialExchangeImporterImpl.kt | 2 -- .../cxf/importer/CredentialExchangeImporterTest.kt | 7 +------ .../cxf/importer/CredentialExchangeImporterTest.kt | 5 ----- 5 files changed, 1 insertion(+), 22 deletions(-) diff --git a/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt b/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt index e8e3cc4dbff..c578fef3aab 100644 --- a/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt +++ b/cxf/src/fdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt @@ -16,8 +16,6 @@ internal class CredentialExchangeImporterImpl( activity: Context, ) : CredentialExchangeImporter { - override fun isSupported(): Boolean = false - override suspend fun importCredentials( credentialTypes: List, ): ImportCredentialsSelectionResult = ImportCredentialsSelectionResult.Failure( diff --git a/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt b/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt index a9affa09334..ee3890b67a0 100644 --- a/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt +++ b/cxf/src/main/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporter.kt @@ -6,13 +6,6 @@ import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult * Responsible for importing credentials from other apps. */ interface CredentialExchangeImporter { - /** - * Returns `true` if credential exchange import is available on the current build, or `false` - * if it is unsupported (e.g. on builds without the Credential Exchange backend). Callers - * should hide or disable import entry points when this returns `false`. - */ - fun isSupported(): Boolean - /** * Starts the import process by requesting selection of a source credential provider. * diff --git a/cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt b/cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt index 04cecf8f1aa..0a08cf43af5 100644 --- a/cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt +++ b/cxf/src/standard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterImpl.kt @@ -24,8 +24,6 @@ internal class CredentialExchangeImporterImpl( ProviderEventsManager.create(activity), ) : CredentialExchangeImporter { - override fun isSupported(): Boolean = true - override suspend fun importCredentials( credentialTypes: List, ): ImportCredentialsSelectionResult = try { diff --git a/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt b/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt index d8db8918beb..78a1f7004f9 100644 --- a/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt +++ b/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt @@ -13,12 +13,7 @@ class CredentialExchangeImporterTest { private val importer = CredentialExchangeImporterImpl( activity = mockk(relaxed = true), ) - - @Test - fun `isSupported should return false`() { - assertFalse(importer.isSupported()) - } - + @Test fun `importCredentials should return Failure`() = runTest { val result = importer.importCredentials(listOf("basic-auth")) diff --git a/cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt b/cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt index 755f7eb867f..90038f726c9 100644 --- a/cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt +++ b/cxf/src/testStandard/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt @@ -57,11 +57,6 @@ class CredentialExchangeImporterTest { ) } - @Test - fun `isSupported should return true`() { - assertTrue(importer.isSupported()) - } - @Test fun `importCredentials should return Success when provider returns valid response`() = runTest { From 37803c34a37073ee0f6ec24f40a8f4efac654493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Thu, 9 Jul 2026 21:55:09 +0100 Subject: [PATCH 4/6] fix unit tests --- fastlane/Fastfile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 36c8d5afbfc..138bf5e54da 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -143,7 +143,14 @@ platform :android do desc "Run library module tests for specified modules" lane :testLibraries do |options| - tasks = options[:target].split(" ").map { |mod| "#{mod}:testDebugUnitTest" } + # Modules with product flavors expose per-flavor test tasks instead of `testDebugUnitTest`. + # For those, run every flavor so both the standard and F-Droid sources are covered. + flavored_module_tasks = { + ":cxf" => ["testStandardDebugUnitTest", "testFdroidDebugUnitTest"], + } + tasks = options[:target].split(" ").flat_map { |mod| + (flavored_module_tasks[mod] || ["testDebugUnitTest"]).map { |task| "#{mod}:#{task}" } + } gradle(tasks: tasks) end From fe0696e39c0e792a94eac041cdbdf0c3aa5ec556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Thu, 9 Jul 2026 21:57:37 +0100 Subject: [PATCH 5/6] format --- .../bitwarden/cxf/importer/CredentialExchangeImporterTest.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt b/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt index 78a1f7004f9..d11bb07d272 100644 --- a/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt +++ b/cxf/src/testFdroid/kotlin/com/bitwarden/cxf/importer/CredentialExchangeImporterTest.kt @@ -4,7 +4,6 @@ import android.app.Activity import com.bitwarden.cxf.importer.model.ImportCredentialsSelectionResult import io.mockk.mockk import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -13,7 +12,7 @@ class CredentialExchangeImporterTest { private val importer = CredentialExchangeImporterImpl( activity = mockk(relaxed = true), ) - + @Test fun `importCredentials should return Failure`() = runTest { val result = importer.importCredentials(listOf("basic-auth")) From 8edf743593781d0efc9dc7786a864ff47b5d3157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lison=20Fernandes?= Date: Thu, 9 Jul 2026 22:08:19 +0100 Subject: [PATCH 6/6] Exclude new testStandard and testFdroid folders from detekt --- detekt-config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/detekt-config.yml b/detekt-config.yml index bf9bdd506d5..fa57c761e03 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -27,7 +27,7 @@ output-reports: comments: active: true - excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**ServiceImpl.kt', '**/testFixtures/**' ] + excludes: [ '**/test/**', '**/testStandard/**', '**/testFdroid/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**ServiceImpl.kt', '**/testFixtures/**' ] AbsentOrWrongFileLicense: active: false licenseTemplateFile: 'license.template'