Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,10 @@ object GiveFeedbackDestination : IntentDirection {
}

object ReportBugDestination : IntentDirection {
@Suppress("ReturnCount")
override fun intent(context: Context): Intent {
val supportEmail = runBlocking {
BackendSupportConfig.resolveEmail(context, context.getString(R.string.send_bug_report_email))
}
if (supportEmail == null) {
BackendSupportConfig.supportPageIntent()?.let { return it }
context.getString(CommonR.string.url_support).takeIf { it.isNotBlank() }?.let {
return Intent(Intent.ACTION_VIEW, Uri.parse(it))
}
}
suspend fun resolveSupportEmail(context: Context): String? =
BackendSupportConfig.resolveEmail(context, context.getString(R.string.send_bug_report_email))

fun intent(context: Context, supportEmail: String?): Intent {
val dir = LogFileWriter.logsDirectory(context)
val logsUris = context.getUrisOfFilesInDirectory(dir)
val intent = context.multipleFileSharingIntent(logsUris)
Expand All @@ -129,6 +121,11 @@ object ReportBugDestination : IntentDirection {
return Intent.createChooser(intent, context.getString(R.string.send_feedback_choose_email))
}

override fun intent(context: Context): Intent = intent(
context = context,
supportEmail = runBlocking { resolveSupportEmail(context) }
)

override val route: String
get() = "wire-intent:report-bug"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/

package com.wire.android.ui.home.settings

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import com.wire.android.R
import com.wire.android.ui.common.VisibilityState
import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.common.visbility.VisibilityState
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.ui.PreviewMultipleThemes

@Composable
internal fun MissingSupportEmailDialog(
dialogState: VisibilityState<Unit>,
onConfirm: () -> Unit
) {
VisibilityState(dialogState) { state ->
WireDialog(
title = stringResource(R.string.report_bug_screen_title),
text = stringResource(R.string.report_bug_missing_support_email_dialog_message),
buttonsHorizontalAlignment = true,
onDismiss = dialogState::dismiss,
dismissButtonProperties = WireDialogButtonProperties(
onClick = dialogState::dismiss,
text = stringResource(R.string.label_cancel),
type = WireDialogButtonType.Secondary
),
optionButton1Properties = WireDialogButtonProperties(
onClick = remember(state) {
{
dialogState.dismiss()
onConfirm()
}
},
text = stringResource(R.string.label_ok),
type = WireDialogButtonType.Primary
)
)
}
}

@PreviewMultipleThemes
@Composable
private fun PreviewMissingSupportEmailDialog() {
WireTheme {
MissingSupportEmailDialog(VisibilityState(isVisible = true)) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.hilt.navigation.compose.hiltViewModel
import com.wire.android.BuildConfig
import com.wire.android.R
Expand All @@ -36,6 +38,7 @@ import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.HomeDestination
import com.wire.android.navigation.HomeNavGraph
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.ReportBugDestination
import com.wire.android.navigation.WireDestination
import com.wire.android.navigation.handleNavigation
import com.wire.android.ui.common.visbility.rememberVisibilityState
Expand All @@ -54,6 +57,7 @@ fun SettingsScreen(
viewModel: SettingsViewModel = hiltViewModel()
) {
val turnAppLockOffDialogState = rememberVisibilityState<Unit>()
val missingSupportEmailDialogState = rememberVisibilityState<Unit>()
val onAppLockSwitchClicked: (Boolean) -> Unit = remember {
{ isChecked ->
if (isChecked) homeStateHolder.navigator.navigate(NavigationCommand(SetLockCodeScreenDestination, BackStackMode.NONE))
Expand All @@ -62,20 +66,39 @@ fun SettingsScreen(
}

val context = LocalContext.current
val staticSupportEmail = stringResource(R.string.send_bug_report_email)
LaunchedEffect(viewModel, context) {
viewModel.reportBugClickAction.collect { action ->
when (action) {
ReportBugClickAction.ConfirmSharingWithoutRecipient -> missingSupportEmailDialogState.show(Unit)
is ReportBugClickAction.Share -> context.startActivity(
ReportBugDestination.intent(context, action.supportEmail)
)
}
}
}
SettingsScreenContent(
lazyListState = homeStateHolder.lazyListStateFor(HomeDestination.Settings),
settingsState = viewModel.state,
onItemClicked = remember {
{
it.direction.handleNavigation(
context = context,
handleOtherDirection = { homeStateHolder.navigator.navigate(NavigationCommand(it)) }
)
onItemClicked = remember(context, homeStateHolder.navigator, staticSupportEmail, viewModel) {
{ item ->
if (item == SettingsItem.ReportBug) {
viewModel.onReportBugClicked(staticSupportEmail)
} else {
item.direction.handleNavigation(
context = context,
handleOtherDirection = { homeStateHolder.navigator.navigate(NavigationCommand(it)) }
)
}
}
},
onAppLockSwitchChanged = onAppLockSwitchClicked
)
TurnAppLockOffDialog(dialogState = turnAppLockOffDialogState, turnOff = viewModel::disableAppLock)
MissingSupportEmailDialog(
dialogState = missingSupportEmailDialogState,
onConfirm = { context.startActivity(ReportBugDestination.intent(context, supportEmail = null)) }
)
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.util.BackendSupportConfig
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.shareIn
Expand All @@ -45,6 +48,9 @@ class SettingsViewModel @Inject constructor(
var state by mutableStateOf(SettingsState())
private set

private val _reportBugClickAction = MutableSharedFlow<ReportBugClickAction>()
internal val reportBugClickAction = _reportBugClickAction.asSharedFlow()

init {
viewModelScope.launch {
combine(
Expand All @@ -69,6 +75,17 @@ class SettingsViewModel @Inject constructor(
}
}

fun onReportBugClicked(staticSupportEmail: String) {
viewModelScope.launch {
val supportEmail = BackendSupportConfig.resolveEmail(globalDataStore, staticSupportEmail)
_reportBugClickAction.emit(
supportEmail?.takeIf(String::isNotBlank)
?.let(ReportBugClickAction::Share)
?: ReportBugClickAction.ConfirmSharingWithoutRecipient
)
}
}

private suspend fun fetchSelfUser() {
viewModelScope.launch {
val self =
Expand All @@ -80,3 +97,8 @@ class SettingsViewModel @Inject constructor(
}
}
}

internal sealed interface ReportBugClickAction {
data class Share(val supportEmail: String) : ReportBugClickAction
data object ConfirmSharingWithoutRecipient : ReportBugClickAction
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ object BackendSupportConfig {
}

suspend fun resolveEmail(context: Context, staticSupportEmail: String): String? {
return resolveEmail(GlobalDataStore(context.applicationContext), staticSupportEmail)
}

suspend fun resolveEmail(globalDataStore: GlobalDataStore, staticSupportEmail: String): String? {
val staticEmail = staticSupportEmail.trim()
return when {
staticEmail.isNotBlank() -> staticEmail
currentBackendApiUrl != null -> currentBackendApiUrl?.let {
GlobalDataStore(context.applicationContext).getBackendSupportEmail(it)
globalDataStore.getBackendSupportEmail(it)
}
else -> null
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,7 @@ Bei Gruppenunterhaltungen kann der Gruppen-Admin diese Einstellung überschreibe
<!-- report bug-->
<string name="send_feedback_choose_email">\"E-Mail-Anbieter auswählen:\"</string>
<string name="send_bug_report_subject">Fehlerbericht</string>
<string name="report_bug_missing_support_email_dialog_message">Keine Support-E-Mail-Adresse ist konfiguriert. Tippen Sie auf „OK“, um den Bericht manuell ohne Empfänger zu teilen.</string>
<string name="backup_dialog_create_backup_save">Datei speichern</string>
<string name="backup_dialog_create_backup_share">Datei teilen</string>
<string name="backup_dialog_restore_incompatible_version_error_title">Inkompatibles Backup</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,7 @@ In group conversations, the group admin can overwrite this setting.</string>
<string name="send_feedback_choose_email">\"Select email provider:\"</string>
<string name="send_bug_report_subject">Bug Report</string>
<string name="send_bug_report_email" translatable="false" >wire-newandroid@wearezeta.zendesk.com</string>
<string name="report_bug_missing_support_email_dialog_message">No support email address is configured. Tap OK to share the report manually without a recipient.</string>

<string name="backup_dialog_create_backup_save">Save File</string>
<string name="backup_dialog_create_backup_share">Share File</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/

package com.wire.android.ui.home.settings

import app.cash.turbine.test
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.TestDispatcherProvider
import com.wire.android.datastore.GlobalDataStore
import com.wire.kalium.logic.feature.featureConfig.ObserveIsAppLockEditableUseCase
import com.wire.kalium.logic.feature.user.ObserveSelfUserUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CoroutineTestExtension::class)
class SettingsViewModelTest {

@Test
fun givenSupportEmail_whenReportBugClicked_thenShareWithRecipient() = runTest {
val (_, viewModel) = Arrangement().arrange()

viewModel.reportBugClickAction.test {
viewModel.onReportBugClicked("support@example.com")

assertEquals(ReportBugClickAction.Share("support@example.com"), awaitItem())
}
}

@Test
fun givenMissingSupportEmail_whenReportBugClicked_thenConfirmSharingWithoutRecipient() = runTest {
val (_, viewModel) = Arrangement().arrange()

viewModel.reportBugClickAction.test {
viewModel.onReportBugClicked("")

assertEquals(ReportBugClickAction.ConfirmSharingWithoutRecipient, awaitItem())
}
}

@Test
fun givenBlankSupportEmail_whenReportBugClicked_thenConfirmSharingWithoutRecipient() = runTest {
val (_, viewModel) = Arrangement().arrange()

viewModel.reportBugClickAction.test {
viewModel.onReportBugClicked(" ")

assertEquals(ReportBugClickAction.ConfirmSharingWithoutRecipient, awaitItem())
}
}

private class Arrangement {
@MockK
lateinit var globalDataStore: GlobalDataStore

@MockK
lateinit var observeIsAppLockEditable: ObserveIsAppLockEditableUseCase

@MockK
lateinit var getSelf: ObserveSelfUserUseCase

init {
MockKAnnotations.init(this, relaxUnitFun = true)
every { globalDataStore.isAppLockPasscodeSetFlow() } returns flowOf(false)
coEvery { observeIsAppLockEditable() } returns flowOf(false)
coEvery { getSelf() } returns emptyFlow()
coEvery { globalDataStore.getBackendSupportEmail(any()) } returns null
}

fun arrange() = this to SettingsViewModel(
globalDataStore = globalDataStore,
observeIsAppLockEditable = observeIsAppLockEditable,
getSelf = getSelf,
dispatchers = TestDispatcherProvider()
)
}
}
Loading