diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt index 06f4ba2c61..151514db0d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequency @@ -41,9 +42,12 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialo import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow +import com.x8bit.bitwarden.ui.platform.components.row.BitwardenExternalLinkRow import com.x8bit.bitwarden.ui.platform.components.row.BitwardenTextRow import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager +import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager /** * Displays the other screen. @@ -54,11 +58,15 @@ import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenWideSwitch fun OtherScreen( onNavigateBack: () -> Unit, viewModel: OtherViewModel = hiltViewModel(), + intentManager: IntentManager = LocalIntentManager.current, ) { val state by viewModel.stateFlow.collectAsState() EventsEffect(viewModel = viewModel) { event -> when (event) { OtherEvent.NavigateBack -> onNavigateBack.invoke() + OtherEvent.NavigateToFeedbackForm -> { + intentManager.launchUri("https://livefrontinc.typeform.com/to/irgrRu4a".toUri()) + } } } @@ -158,6 +166,16 @@ fun OtherScreen( .semantics { testTag = "AllowScreenCaptureSwitch" } .padding(horizontal = 16.dp), ) + + BitwardenExternalLinkRow( + text = stringResource(R.string.give_feedback), + onConfirmClick = remember(viewModel) { + { viewModel.trySendAction(OtherAction.GiveFeedbackClick) } + }, + dialogTitle = stringResource(R.string.continue_to_give_feedback), + dialogMessage = stringResource(R.string.continue_to_provide_feedback), + withDivider = false, + ) } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt index a19f32a010..057c4b79c4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModel.kt @@ -60,6 +60,7 @@ class OtherViewModel @Inject constructor( is OtherAction.AllowSyncToggle -> handleAllowSyncToggled(action) OtherAction.BackClick -> handleBackClicked() is OtherAction.ClearClipboardFrequencyChange -> handleClearClipboardFrequencyChanged(action) + OtherAction.GiveFeedbackClick -> handleGiveFeedbackClicked() OtherAction.SyncNowButtonClick -> handleSyncNowButtonClicked() is OtherAction.Internal -> handleInternalAction(action) } @@ -87,6 +88,10 @@ class OtherViewModel @Inject constructor( settingsRepo.clearClipboardFrequency = action.clearClipboardFrequency } + private fun handleGiveFeedbackClicked() { + sendEvent(OtherEvent.NavigateToFeedbackForm) + } + private fun handleSyncNowButtonClicked() { mutableStateFlow.update { it.copy(dialogState = OtherState.DialogState.Loading(R.string.syncing.asText())) @@ -146,6 +151,11 @@ sealed class OtherEvent { * Navigate back. */ data object NavigateBack : OtherEvent() + + /** + * Navigate to the feedback form. + */ + data object NavigateToFeedbackForm : OtherEvent() } /** @@ -178,6 +188,11 @@ sealed class OtherAction { val clearClipboardFrequency: ClearClipboardFrequency, ) : OtherAction() + /** + * Indicates that the user clicked the Give feedback button. + */ + data object GiveFeedbackClick : OtherAction() + /** * Indicates that the user clicked the Sync Now button. */ diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml index a8403490fa..bf2781c7c6 100644 --- a/app/src/main/res/values/strings_non_localized.xml +++ b/app/src/main/res/values/strings_non_localized.xml @@ -3,4 +3,8 @@ Bitwarden Dev Duo Duo (%1$s) + + Give feedback + Continue to Give feedback? + Select continue to provide feedback on your experience in a web form. diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt index c708281ebd..09fe4d4deb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherScreenTest.kt @@ -13,6 +13,7 @@ import com.x8bit.bitwarden.data.platform.repository.model.ClearClipboardFrequenc import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager import com.x8bit.bitwarden.ui.util.assertNoDialogExists import io.mockk.every import io.mockk.mockk @@ -32,6 +33,9 @@ class OtherScreenTest : BaseComposeTest() { every { eventFlow } returns mutableEventFlow every { stateFlow } returns mutableStateFlow } + private val intentManager: IntentManager = mockk { + every { getShareDataFromIntent(any()) } returns null + } @Before fun setup() { @@ -39,6 +43,7 @@ class OtherScreenTest : BaseComposeTest() { OtherScreen( viewModel = viewModel, onNavigateBack = { haveCalledNavigateBack = true }, + intentManager = intentManager, ) } } @@ -144,6 +149,22 @@ class OtherScreenTest : BaseComposeTest() { .assertIsDisplayed() .assert(hasAnyAncestor(isDialog())) } + + @Suppress("MaxLineLength") + @Test + fun `on give feedback click should display confirmation dialog and confirm click should emit GiveFeedbackClick`() { + composeTestRule.onNode(isDialog()).assertDoesNotExist() + composeTestRule.onNodeWithText("Give feedback").performClick() + composeTestRule.onNode(isDialog()).assertExists() + composeTestRule + .onAllNodesWithText("Continue") + .filterToOne(hasAnyAncestor(isDialog())) + .performClick() + composeTestRule.onNode(isDialog()).assertDoesNotExist() + verify { + viewModel.trySendAction(OtherAction.GiveFeedbackClick) + } + } } private val DEFAULT_STATE = OtherState( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt index d85f2d9740..1555694994 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/other/OtherViewModelTest.kt @@ -108,6 +108,15 @@ class OtherViewModelTest : BaseViewModelTest() { } } + @Test + fun `on GiveFeedbackClick should emit NavigateToFeedbackForm`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(OtherAction.GiveFeedbackClick) + assertEquals(OtherEvent.NavigateToFeedbackForm, awaitItem()) + } + } + @Test fun `on ClearClipboardFrequencyChange should update state`() = runTest { val viewModel = createViewModel()