summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-12 16:11:38 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-13 15:48:32 +0200
commitc0b2a8c0f1c565a0343e91e914accafe1e69f3fc (patch)
tree0c7179607e59043cc564721b5161ca5fff7af7f2 /android
parentce62828a21cb65f2b0d1710b7d92b4e55a518408 (diff)
downloadmullvadvpn-c0b2a8c0f1c565a0343e91e914accafe1e69f3fc.tar.xz
mullvadvpn-c0b2a8c0f1c565a0343e91e914accafe1e69f3fc.zip
Show a specific error if selected port is not in the port range
Diffstat (limited to 'android')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt57
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt88
-rw-r--r--android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt2
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml2
-rw-r--r--android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NotificationBannerTv.kt6
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/AnimatedNotificationBanner.kt2
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NotificationData.kt54
11 files changed, 213 insertions, 15 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
index 14409f1fb2..1bed735bb0 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreenTest.kt
@@ -72,6 +72,7 @@ class ConnectScreenTest {
onChangelogClick: () -> Unit = {},
onDismissChangelogClick: () -> Unit = {},
onNavigateToFeature: (FeatureIndicator) -> Unit = {},
+ onClickShowWireguardPortSettings: () -> Unit = {},
) {
setContentWithTheme {
ConnectScreen(
@@ -89,6 +90,7 @@ class ConnectScreenTest {
onChangelogClick = onChangelogClick,
onDismissChangelogClick = onDismissChangelogClick,
onNavigateToFeature = onNavigateToFeature,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
index 9ff9ec5a00..1605b83cdb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/notificationbanner/NotificationBanner.kt
@@ -55,6 +55,7 @@ private fun PreviewNotificationBanner() {
onClickShowChangelog = {},
onClickDismissChangelog = {},
onClickDismissNewDevice = {},
+ onClickShowWireguardPortSettings = {},
)
Spacer(modifier = Modifier.size(16.dp))
}
@@ -72,6 +73,7 @@ fun NotificationBanner(
onClickShowChangelog: () -> Unit,
onClickDismissChangelog: () -> Unit,
onClickDismissNewDevice: () -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
if (isTv()) {
NotificationBannerTv(
@@ -83,6 +85,7 @@ fun NotificationBanner(
onClickShowChangelog = onClickShowChangelog,
onClickDismissChangelog = onClickDismissChangelog,
onClickDismissNewDevice = onClickDismissNewDevice,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
} else {
AnimatedNotificationBanner(
@@ -95,6 +98,7 @@ fun NotificationBanner(
onClickShowChangelog = onClickShowChangelog,
onClickDismissChangelog = onClickDismissChangelog,
onClickDismissNewDevice = onClickDismissNewDevice,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index 94db024b41..26c08e4841 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -164,6 +164,7 @@ private fun PreviewAccountScreen(
{},
{},
{},
+ {},
)
}
}
@@ -304,12 +305,15 @@ fun Connect(
dropUnlessResumed { feature: FeatureIndicator ->
navigator.navigate(feature.destination())
},
+ onClickShowWireguardPortSettings =
+ dropUnlessResumed { navigator.navigate(VpnSettingsDestination()) },
)
}
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
+@Suppress("LongParameterList")
fun ConnectScreen(
state: ConnectUiState,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
@@ -326,6 +330,7 @@ fun ConnectScreen(
onAccountClick: () -> Unit,
onDismissNewDeviceClick: () -> Unit,
onNavigateToFeature: (FeatureIndicator) -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
val contentFocusRequester = remember { FocusRequester() }
@@ -346,6 +351,7 @@ fun ConnectScreen(
onDismissChangelogClick,
onDismissNewDeviceClick,
onNavigateToFeature,
+ onClickShowWireguardPortSettings,
)
}
@@ -384,6 +390,7 @@ fun ConnectScreen(
}
@Composable
+@Suppress("LongParameterList")
private fun Content(
focusRequester: FocusRequester,
paddingValues: PaddingValues,
@@ -399,6 +406,7 @@ private fun Content(
onDismissChangelogClick: () -> Unit,
onDismissNewDeviceClick: () -> Unit,
onNavigateToFeature: (FeatureIndicator) -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
val screenHeight = LocalWindowInfo.current.containerSize.height.dp
val indicatorPercentOffset =
@@ -446,6 +454,7 @@ private fun Content(
onClickShowChangelog = onChangelogClick,
onClickDismissChangelog = onDismissChangelogClick,
onClickDismissNewDevice = onDismissNewDeviceClick,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
ConnectionCard(
state = state,
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 c44aedd890..c9c1f1e78a 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
@@ -153,7 +153,7 @@ val uiModule = module {
single { WireguardConstraintsRepository(get()) }
single { AccountExpiryInAppNotificationUseCase(get()) }
- single { TunnelStateNotificationUseCase(get()) }
+ single { TunnelStateNotificationUseCase(get(), get(), get()) }
single { VersionNotificationUseCase(get(), BuildConfig.ENABLE_IN_APP_VERSION_NOTIFICATIONS) }
single { NewDeviceNotificationUseCase(get(), get()) }
single { NewChangelogNotificationUseCase(get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
index 85ea7cf11a..753f8c1eef 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCase.kt
@@ -1,18 +1,46 @@
package net.mullvad.mullvadvpn.usecase
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.Constraint
+import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause
import net.mullvad.mullvadvpn.lib.model.InAppNotification
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.repository.SettingsRepository
+import net.mullvad.mullvadvpn.util.inAnyOf
-class TunnelStateNotificationUseCase(private val connectionProxy: ConnectionProxy) {
+class TunnelStateNotificationUseCase(
+ private val connectionProxy: ConnectionProxy,
+ private val relayListRepository: RelayListRepository,
+ private val settingsRepository: SettingsRepository,
+) {
+ @OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(): Flow<List<InAppNotification>> =
connectionProxy.tunnelState
.distinctUntilChanged()
.map(::tunnelStateNotification)
+ .flatMapLatest { inAppNotification ->
+ combine(relayListRepository.portRanges, settingsRepository.settingsUpdates) {
+ portRanges,
+ settings ->
+ inAppNotification?.maybeUpdateWithPortError(
+ wireguardPort = settings.wireguardPort(),
+ availablePorts = portRanges,
+ )
+ }
+ }
.map(::listOfNotNull)
.distinctUntilChanged()
@@ -31,4 +59,31 @@ class TunnelStateNotificationUseCase(private val connectionProxy: ConnectionProx
is TunnelState.Connected,
is TunnelState.Disconnected -> null
}
+
+ private fun InAppNotification.maybeUpdateWithPortError(
+ wireguardPort: Constraint<Port>,
+ availablePorts: List<PortRange>,
+ ): InAppNotification =
+ if (this is InAppNotification.TunnelStateError && error.isPossiblePortError()) {
+ wireguardPort.invalidPortOrNull(availablePorts)?.let {
+ copy(
+ error =
+ ErrorState(
+ cause = ErrorStateCause.NoRelaysMatchSelectedPort(port = it),
+ isBlocking = error.isBlocking,
+ )
+ )
+ } ?: this
+ } else this
+
+ private fun ErrorState.isPossiblePortError(): Boolean =
+ cause is ErrorStateCause.TunnelParameterError &&
+ (cause as ErrorStateCause.TunnelParameterError).error ==
+ ParameterGenerationError.NoMatchingRelay
+
+ private fun Constraint<Port>.invalidPortOrNull(availablePortRanges: List<PortRange>): Port? =
+ getOrNull()?.takeIf { !it.inAnyOf(availablePortRanges) }
+
+ private fun Settings?.wireguardPort() =
+ this?.relaySettings?.relayConstraints?.wireguardConstraints?.port ?: Constraint.Any
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
index 8d2ece124b..6544913748 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/TunnelStateNotificationUseCaseTest.kt
@@ -5,17 +5,27 @@ import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
+import net.mullvad.mullvadvpn.lib.common.test.assertLists
import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect
+import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.ErrorState
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.NoRelaysMatchSelectedPort
+import net.mullvad.mullvadvpn.lib.model.ErrorStateCause.TunnelParameterError
import net.mullvad.mullvadvpn.lib.model.InAppNotification
+import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError
+import net.mullvad.mullvadvpn.lib.model.Port
+import net.mullvad.mullvadvpn.lib.model.PortRange
+import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.repository.SettingsRepository
import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -24,18 +34,28 @@ import org.junit.jupiter.api.extension.ExtendWith
class TunnelStateNotificationUseCaseTest {
private val mockConnectionProxy: ConnectionProxy = mockk()
+ private val mockRelayListRepository: RelayListRepository = mockk()
+ private val mockSettingsRepository: SettingsRepository = mockk()
private lateinit var tunnelStateNotificationUseCase: TunnelStateNotificationUseCase
private val tunnelState = MutableStateFlow<TunnelState>(TunnelState.Disconnected())
+ private val portRanges = MutableStateFlow<List<PortRange>>(emptyList())
+ private val settingsFlow = MutableStateFlow<Settings>(mockk(relaxed = true))
@BeforeEach
fun setup() {
MockKAnnotations.init(this)
every { mockConnectionProxy.tunnelState } returns tunnelState
+ every { mockRelayListRepository.portRanges } returns portRanges
+ every { mockSettingsRepository.settingsUpdates } returns settingsFlow
tunnelStateNotificationUseCase =
- TunnelStateNotificationUseCase(connectionProxy = mockConnectionProxy)
+ TunnelStateNotificationUseCase(
+ connectionProxy = mockConnectionProxy,
+ relayListRepository = mockRelayListRepository,
+ settingsRepository = mockSettingsRepository,
+ )
}
@AfterEach
@@ -46,15 +66,16 @@ class TunnelStateNotificationUseCaseTest {
@Test
fun `initial state should be empty`() = runTest {
// Arrange, Act, Assert
- tunnelStateNotificationUseCase().test { assertTrue { awaitItem().isEmpty() } }
+ tunnelStateNotificationUseCase().test { assertTrue(awaitItem().isEmpty()) }
}
@Test
fun `when TunnelState is error use case should emit TunnelStateError notification`() = runTest {
tunnelStateNotificationUseCase().test {
// Arrange, Act
- assertEquals(emptyList(), awaitItem())
+ assertLists(emptyList(), awaitItem())
val errorState: ErrorState = mockk()
+ every { errorState.cause } returns mockk()
tunnelState.emit(TunnelState.Error(errorState))
// Assert
@@ -67,11 +88,66 @@ class TunnelStateNotificationUseCaseTest {
runTest {
tunnelStateNotificationUseCase().test {
// Arrange, Act
- assertEquals(emptyList(), awaitItem())
+ assertLists(emptyList(), awaitItem())
tunnelState.emit(TunnelState.Disconnecting(ActionAfterDisconnect.Block))
// Assert
assertEquals(listOf(InAppNotification.TunnelStateBlocked), awaitItem())
}
}
+
+ @Test
+ fun `when error cause is TunnelParameterError and port is not in range use case should emit NoRelaysMatchSelectedPort error`() =
+ runTest {
+ tunnelStateNotificationUseCase().test {
+ // Arrange, Act
+ assertLists(emptyList(), awaitItem())
+ val errorState: ErrorState = mockk()
+ every { errorState.isBlocking } returns true
+ every { errorState.cause } returns
+ TunnelParameterError(ParameterGenerationError.NoMatchingRelay)
+ val settings: Settings = mockk()
+ every { settings.relaySettings.relayConstraints.wireguardConstraints.port } returns
+ Constraint.Only(Port(1))
+ val portRange = PortRange(2..3)
+ settingsFlow.emit(settings)
+ portRanges.emit(listOf(portRange))
+ tunnelState.emit(TunnelState.Error(errorState))
+
+ // Assert
+ val item = awaitItem()
+ assertTrue {
+ (item.first() as InAppNotification.TunnelStateError).error.cause is
+ NoRelaysMatchSelectedPort
+ }
+ }
+ }
+
+ @Test
+ fun `when error cause is TunnelParameterError and port is in range use case should emit TunnelParameterError error`() =
+ runTest {
+ tunnelStateNotificationUseCase().test {
+ // Arrange, Act
+ assertLists(emptyList(), awaitItem())
+ val errorState: ErrorState = mockk()
+ every { errorState.isBlocking } returns true
+ every { errorState.cause } returns
+ TunnelParameterError(ParameterGenerationError.NoMatchingRelay)
+ val settings: Settings = mockk()
+ every { settings.relaySettings.relayConstraints.wireguardConstraints.port } returns
+ Constraint.Only(Port(2))
+ val portRange = PortRange(2..3)
+ settingsFlow.emit(settings)
+ portRanges.emit(listOf(portRange))
+ tunnelState.emit(TunnelState.Error(errorState))
+
+ // Assert
+ val item = awaitItem()
+ assertEquals(listOf(InAppNotification.TunnelStateError(errorState)), item)
+ assertTrue {
+ (item.first() as InAppNotification.TunnelStateError).error.cause is
+ TunnelParameterError
+ }
+ }
+ }
}
diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt
index fdb7dd3a1a..cd65a7bc97 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt
@@ -30,6 +30,8 @@ sealed class ErrorStateCause {
data class OtherAlwaysOnApp(val appName: String) : ErrorStateCause()
data object OtherLegacyAlwaysOnApp : ErrorStateCause()
+
+ data class NoRelaysMatchSelectedPort(val port: Port) : ErrorStateCause()
}
sealed interface AuthFailedError {
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index f96fa9668b..e7405020d0 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -417,4 +417,6 @@
<string name="daita_multihop">%s: Multihop</string>
<string name="vpn_permission_denied_error_no_vpn_settings">VPN permission was denied</string>
<string name="vpn_settings_not_available">VPN Settings not available on device</string>
+ <string name="wireguard_port_is_not_supported">The selected %s port is not supported, please change it under</string>
+ <string name="wireguard_settings">%s settings.</string>
</resources>
diff --git a/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NotificationBannerTv.kt b/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NotificationBannerTv.kt
index 97d986c36a..8868938a12 100644
--- a/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NotificationBannerTv.kt
+++ b/android/lib/tv/src/main/kotlin/net/mullvad/mullvadvpn/lib/tv/NotificationBannerTv.kt
@@ -24,7 +24,9 @@ fun PreviewNotificationBannerTv() {
onClickShowAccount = {},
onClickShowChangelog = {},
onClickDismissChangelog = {},
- ) {}
+ onClickDismissNewDevice = {},
+ onClickShowWireguardPortSettings = {},
+ )
}
}
@@ -38,6 +40,7 @@ fun NotificationBannerTv(
onClickShowChangelog: () -> Unit,
onClickDismissChangelog: () -> Unit,
onClickDismissNewDevice: () -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
AnimatedNotificationBanner(
modifier = modifier,
@@ -59,5 +62,6 @@ fun NotificationBannerTv(
onClickShowChangelog = onClickShowChangelog,
onClickDismissChangelog = onClickDismissChangelog,
onClickDismissNewDevice = onClickDismissNewDevice,
+ onClickShowWireguardPortSettings = onClickShowWireguardPortSettings,
)
}
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/AnimatedNotificationBanner.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/AnimatedNotificationBanner.kt
index 979f63920a..ba0a936109 100644
--- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/AnimatedNotificationBanner.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/AnimatedNotificationBanner.kt
@@ -44,6 +44,7 @@ fun AnimatedNotificationBanner(
onClickShowChangelog: () -> Unit,
onClickDismissChangelog: () -> Unit,
onClickDismissNewDevice: () -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) {
// Fix for animating to invisible state
val previous = rememberPrevious(current = notification, shouldUpdate = { _, _ -> true })
@@ -64,6 +65,7 @@ fun AnimatedNotificationBanner(
onClickShowChangelog,
onClickDismissChangelog,
onClickDismissNewDevice,
+ onClickShowWireguardPortSettings,
),
)
}
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NotificationData.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NotificationData.kt
index b022ca8c07..c9bfcd887d 100644
--- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NotificationData.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/NotificationData.kt
@@ -9,10 +9,13 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.LinkInteractionListener
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.core.text.HtmlCompat
import java.net.InetAddress
@@ -75,6 +78,7 @@ fun InAppNotification.toNotificationData(
onClickShowChangelog: () -> Unit,
onClickDismissChangelog: () -> Unit,
onClickDismissNewDevice: () -> Unit,
+ onClickShowWireguardPortSettings: () -> Unit,
) =
when (this) {
is InAppNotification.NewDevice ->
@@ -113,7 +117,8 @@ fun InAppNotification.toNotificationData(
title = stringResource(id = R.string.blocking_internet),
statusLevel = StatusLevel.Error,
)
- is InAppNotification.TunnelStateError -> errorMessageBannerData(error)
+ is InAppNotification.TunnelStateError ->
+ errorMessageBannerData(error, onClickShowWireguardPortSettings)
is InAppNotification.UnsupportedVersion ->
NotificationData(
title = stringResource(id = R.string.unsupported_version),
@@ -156,10 +161,13 @@ fun InAppNotification.toNotificationData(
}
@Composable
-private fun errorMessageBannerData(error: ErrorState) =
+private fun errorMessageBannerData(
+ error: ErrorState,
+ onClickShowWireguardPortSettings: () -> Unit,
+) =
NotificationData(
title = error.title().formatWithHtml(),
- message = NotificationMessage.Text(error.message().formatWithHtml()),
+ message = NotificationMessage.Text(error.message(onClickShowWireguardPortSettings)),
statusLevel = StatusLevel.Error,
)
@@ -191,11 +199,13 @@ private fun ErrorState.title(): String {
}
@Composable
-private fun ErrorState.message(): String {
+private fun ErrorState.message(onClickShowWireguardPortSettings: () -> Unit): AnnotatedString {
val cause = this.cause
return when {
- isBlocking -> cause.errorMessageId()
- else -> stringResource(R.string.failed_to_block_internet)
+ cause is ErrorStateCause.NoRelaysMatchSelectedPort ->
+ cause.message(onClickShowWireguardPortSettings)
+ isBlocking -> cause.errorMessageId().formatWithHtml()
+ else -> stringResource(R.string.failed_to_block_internet).formatWithHtml()
}
}
@@ -220,6 +230,8 @@ private fun ErrorStateCause.errorMessageId(): String =
R.string.invalid_dns_servers,
addresses.joinToString { address -> address.addressString() },
)
+ is ErrorStateCause.NoRelaysMatchSelectedPort ->
+ stringResource(R.string.wireguard_port_is_not_supported)
}
private fun AuthFailedError.errorMessageId(): Int =
@@ -249,3 +261,33 @@ private fun InetAddress.addressString(): String {
return address
}
+
+@Composable
+private fun ErrorStateCause.NoRelaysMatchSelectedPort.message(
+ onClickShowWireguardPortSettings: () -> Unit
+) = buildAnnotatedString {
+ append(
+ stringResource(R.string.wireguard_port_is_not_supported, stringResource(R.string.wireguard))
+ )
+ append(" ")
+ withStyle(
+ SpanStyle(
+ color = MaterialTheme.colorScheme.onSurface,
+ textDecoration = TextDecoration.Underline,
+ )
+ ) {
+ withLink(
+ LinkAnnotation.Clickable(
+ tag = stringResource(R.string.wireguard),
+ linkInteractionListener =
+ object : LinkInteractionListener {
+ override fun onClick(link: LinkAnnotation) {
+ onClickShowWireguardPortSettings()
+ }
+ },
+ )
+ ) {
+ append(stringResource(R.string.wireguard_settings, stringResource(R.string.wireguard)))
+ }
+ }
+}