summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-10-06 16:32:14 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-10-15 07:23:21 +0200
commite06c8ede52f8fdc7ae974038757674a36c527019 (patch)
tree1248b168e5c33129a7ea1e31febe5ca12df965a6 /android/app/src
parent66d1b4e66ae05913e0fa557ac123758a11f7329d (diff)
downloadmullvadvpn-e06c8ede52f8fdc7ae974038757674a36c527019.tar.xz
mullvadvpn-e06c8ede52f8fdc7ae974038757674a36c527019.zip
Enable the user to include their account token in problem reports
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ReportProblemUiStatePreviewParameterProvider.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt70
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt24
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt25
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt19
6 files changed, 120 insertions, 23 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ReportProblemUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ReportProblemUiStatePreviewParameterProvider.kt
index 52822a316a..f878ae9d37 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ReportProblemUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ReportProblemUiStatePreviewParameterProvider.kt
@@ -10,7 +10,7 @@ class ReportProblemUiStatePreviewParameterProvider :
override val values: Sequence<ReportProblemUiState>
get() =
sequenceOf(
- ReportProblemUiState(),
+ ReportProblemUiState(showIncludeAccountId = true),
ReportProblemUiState(sendingState = SendingReportUiState.Sending),
ReportProblemUiState(sendingState = SendingReportUiState.Success("email@mail.com")),
ReportProblemUiState(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt
index 699a71a164..ca06b04e74 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt
@@ -1,9 +1,11 @@
package net.mullvad.mullvadvpn.compose.screen
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -11,13 +13,14 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -60,9 +63,9 @@ import net.mullvad.mullvadvpn.compose.textfield.mullvadWhiteTextFieldColors
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.compose.util.SecureScreenWhileInView
-import net.mullvad.mullvadvpn.compose.util.toDp
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.ui.designsystem.Checkbox
import net.mullvad.mullvadvpn.viewmodel.ReportProblemSideEffect
import net.mullvad.mullvadvpn.viewmodel.ReportProblemUiState
import net.mullvad.mullvadvpn.viewmodel.ReportProblemViewModel
@@ -83,6 +86,7 @@ private fun PreviewReportProblemScreen(
onNavigateToViewLogs = {},
onEmailChanged = {},
onDescriptionChanged = {},
+ onIncludeAccountIdCheckChange = {},
onBackClick = {},
)
}
@@ -121,6 +125,7 @@ fun ReportProblem(
},
onEmailChanged = vm::updateEmail,
onDescriptionChanged = vm::updateDescription,
+ onIncludeAccountIdCheckChange = vm::onIncludeAccountIdCheckChange,
onBackClick = dropUnlessResumed { navigator.navigateUp() },
)
}
@@ -133,6 +138,7 @@ private fun ReportProblemScreen(
onNavigateToViewLogs: () -> Unit,
onEmailChanged: (String) -> Unit,
onDescriptionChanged: (String) -> Unit,
+ onIncludeAccountIdCheckChange: (Boolean) -> Unit,
onBackClick: () -> Unit,
) {
@@ -169,10 +175,9 @@ private fun ReportProblemScreen(
.height(IntrinsicSize.Max),
verticalArrangement = Arrangement.spacedBy(Dimens.mediumPadding),
) {
- Text(
- text = stringResource(id = R.string.problem_report_description),
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- style = MaterialTheme.typography.labelLarge,
+ Description(
+ state = state,
+ onIncludeAccountIdCheckChange = onIncludeAccountIdCheckChange,
)
TextField(
@@ -215,7 +220,58 @@ private fun ReportProblemScreen(
}
@Composable
-fun ProblemMessageTextField(
+private fun Description(
+ state: ReportProblemUiState,
+ onIncludeAccountIdCheckChange: (Boolean) -> Unit,
+) {
+ Column {
+ Text(
+ text = stringResource(id = R.string.problem_report_description),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ )
+
+ if (state.showIncludeAccountId) {
+ IncludeAccountInformationCheckBox(
+ includeAccountInformation = state.includeAccountId,
+ onIncludeAccountInformationCheckChange = onIncludeAccountIdCheckChange,
+ )
+ }
+ }
+}
+
+@Composable
+private fun IncludeAccountInformationCheckBox(
+ includeAccountInformation: Boolean,
+ onIncludeAccountInformationCheckChange: (Boolean) -> Unit,
+) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier =
+ Modifier.clickable {
+ onIncludeAccountInformationCheckChange(!includeAccountInformation)
+ }
+ .padding(vertical = Dimens.smallPadding)
+ .fillMaxWidth(),
+ ) {
+ // To align the checkbox with the text
+ CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
+ Checkbox(
+ modifier = Modifier.padding(end = Dimens.smallPadding),
+ checked = includeAccountInformation,
+ onCheckedChange = onIncludeAccountInformationCheckChange,
+ )
+ Text(
+ text = stringResource(R.string.include_account_token_checkbox_text),
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.labelLarge,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ProblemMessageTextField(
modifier: Modifier = Modifier,
value: String,
onDescriptionChanged: (String) -> Unit,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt
index 3b4a460fea..d02555db00 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt
@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointFromIntentHolder
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointOverride
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.service.BuildConfig
const val PROBLEM_REPORT_LOGS_FILE = "problem_report.txt"
@@ -28,6 +29,7 @@ class MullvadProblemReport(
context: Context,
private val apiEndpointOverride: ApiEndpointOverride?,
private val apiEndpointFromIntentHolder: ApiEndpointFromIntentHolder,
+ private val accountRepository: AccountRepository,
val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
@@ -47,7 +49,10 @@ class MullvadProblemReport(
collectReport(logDirectory.absolutePath, logsPath.absolutePath)
}
- suspend fun sendReport(userReport: UserReport): SendProblemReportResult {
+ suspend fun sendReport(
+ userReport: UserReport,
+ includeAccountId: Boolean,
+ ): SendProblemReportResult {
// If report is not collected then, collect it, if it fails then return error
if (!logsExists() && !collectLogs()) {
return SendProblemReportResult.Error.CollectLog
@@ -64,11 +69,17 @@ class MullvadProblemReport(
}
sendProblemReport(
- userReport.email ?: "",
- userReport.description,
- logsPath.absolutePath,
- cacheDirectory.absolutePath,
- apiOverride,
+ userEmail = userReport.email ?: "",
+ userMessage = userReport.description,
+ accountId =
+ if (includeAccountId) {
+ accountRepository.accountData.value?.id?.value?.toString()
+ } else {
+ null
+ },
+ reportPath = logsPath.absolutePath,
+ cacheDirectory = cacheDirectory.absolutePath,
+ apiEndpointOverride = apiOverride,
)
}
@@ -104,6 +115,7 @@ class MullvadProblemReport(
private external fun sendProblemReport(
userEmail: String,
userMessage: String,
+ accountId: String?,
reportPath: String,
cacheDirectory: String,
apiEndpointOverride: ApiEndpointOverride?,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index 6839b76753..7b2ae9986e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -137,6 +137,7 @@ val uiModule = module {
context = androidContext(),
apiEndpointOverride = getOrNull(),
apiEndpointFromIntentHolder = get(),
+ accountRepository = get(),
)
}
single { RelayOverridesRepository(get()) }
@@ -268,7 +269,7 @@ val uiModule = module {
viewModel { VoucherDialogViewModel(get()) }
viewModel { VpnSettingsViewModel(get(), get(), get(), get(), get(), get()) }
viewModel { WelcomeViewModel(get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
- viewModel { ReportProblemViewModel(get(), get()) }
+ viewModel { ReportProblemViewModel(get(), get(), get()) }
viewModel { ViewLogsViewModel(get()) }
viewModel { OutOfTimeViewModel(get(), get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
viewModel { FilterViewModel(get(), get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt
index 0f27ea1516..c07f0d5df1 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModel.kt
@@ -17,12 +17,15 @@ import net.mullvad.mullvadvpn.constant.VIEW_MODEL_STOP_TIMEOUT
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.dataproxy.SendProblemReportResult
import net.mullvad.mullvadvpn.dataproxy.UserReport
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.repository.ProblemReportRepository
data class ReportProblemUiState(
val sendingState: SendingReportUiState? = null,
val email: String = "",
val description: String = "",
+ val showIncludeAccountId: Boolean = false,
+ val includeAccountId: Boolean = false,
)
sealed interface SendingReportUiState {
@@ -40,16 +43,25 @@ sealed interface ReportProblemSideEffect {
class ReportProblemViewModel(
private val mullvadProblemReporter: MullvadProblemReport,
private val problemReportRepository: ProblemReportRepository,
+ accountRepository: AccountRepository,
) : ViewModel() {
private val sendingState: MutableStateFlow<SendingReportUiState?> = MutableStateFlow(null)
+ private val includeAccountIdState: MutableStateFlow<Boolean> = MutableStateFlow(false)
val uiState =
- combine(sendingState, problemReportRepository.problemReport) { pendingState, userReport ->
+ combine(
+ sendingState,
+ includeAccountIdState,
+ problemReportRepository.problemReport,
+ accountRepository.accountData,
+ ) { sendingState, includeAccountToken, userReport, accountData ->
ReportProblemUiState(
- sendingState = pendingState,
+ sendingState = sendingState,
email = userReport.email ?: "",
description = userReport.description,
+ showIncludeAccountId = accountData != null,
+ includeAccountId = includeAccountToken,
)
}
.stateIn(
@@ -72,7 +84,10 @@ class ReportProblemViewModel(
// Ensure we show loading for at least MINIMUM_LOADING_TIME_MILLIS
val deferredResult = async {
- mullvadProblemReporter.sendReport(UserReport(nullableEmail, description))
+ mullvadProblemReporter.sendReport(
+ UserReport(nullableEmail, description),
+ includeAccountIdState.value,
+ )
}
delay(MINIMUM_LOADING_TIME_MILLIS)
@@ -99,6 +114,10 @@ class ReportProblemViewModel(
problemReportRepository.setDescription(description)
}
+ fun onIncludeAccountIdCheckChange(checked: Boolean) {
+ includeAccountIdState.tryEmit(checked)
+ }
+
private fun shouldShowConfirmNoEmail(userEmail: String?): Boolean =
userEmail.isNullOrEmpty() && uiState.value.sendingState !is SendingReportUiState
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt
index fa5ec10b5c..e2fccb48ed 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ReportProblemViewModelTest.kt
@@ -14,6 +14,7 @@ import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.dataproxy.SendProblemReportResult
import net.mullvad.mullvadvpn.dataproxy.UserReport
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.repository.ProblemReportRepository
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -27,6 +28,8 @@ class ReportProblemViewModelTest {
@MockK(relaxed = true) private lateinit var mockProblemReportRepository: ProblemReportRepository
+ @MockK(relaxed = true) private lateinit var mockAccountRepository: AccountRepository
+
private val problemReportFlow = MutableStateFlow(UserReport("", ""))
private lateinit var viewModel: ReportProblemViewModel
@@ -36,7 +39,13 @@ class ReportProblemViewModelTest {
MockKAnnotations.init(this)
coEvery { mockMullvadProblemReport.collectLogs() } returns true
coEvery { mockProblemReportRepository.problemReport } returns problemReportFlow
- viewModel = ReportProblemViewModel(mockMullvadProblemReport, mockProblemReportRepository)
+ coEvery { mockAccountRepository.accountData } returns MutableStateFlow(null)
+ viewModel =
+ ReportProblemViewModel(
+ mockMullvadProblemReport,
+ mockProblemReportRepository,
+ mockAccountRepository,
+ )
}
@AfterEach
@@ -48,7 +57,7 @@ class ReportProblemViewModelTest {
fun `when sendReport returns CollectLog error then uiState should emit sendingState with CollectLog error`() =
runTest {
// Arrange
- coEvery { mockMullvadProblemReport.sendReport(any()) } returns
+ coEvery { mockMullvadProblemReport.sendReport(any(), any()) } returns
SendProblemReportResult.Error.CollectLog
val email = "my@email.com"
@@ -68,7 +77,7 @@ class ReportProblemViewModelTest {
fun `when sendReport returns SendReport error then uiState should emit sendingState with SendReport error`() =
runTest {
// Arrange
- coEvery { mockMullvadProblemReport.sendReport(any()) } returns
+ coEvery { mockMullvadProblemReport.sendReport(any(), any()) } returns
SendProblemReportResult.Error.SendReport
val email = "my@email.com"
@@ -88,7 +97,7 @@ class ReportProblemViewModelTest {
fun `when sendReport with no email returns Success then uiState should emit sendingState with Success`() =
runTest {
// Arrange
- coEvery { mockMullvadProblemReport.sendReport(any()) } returns
+ coEvery { mockMullvadProblemReport.sendReport(any(), any()) } returns
SendProblemReportResult.Success
val email = ""
val description = "My description"
@@ -121,7 +130,7 @@ class ReportProblemViewModelTest {
runTest {
// Arrange
coEvery { mockMullvadProblemReport.collectLogs() } returns true
- coEvery { mockMullvadProblemReport.sendReport(any()) } returns
+ coEvery { mockMullvadProblemReport.sendReport(any(), any()) } returns
SendProblemReportResult.Success
val email = "my@email.com"
val description = "My description"