diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-05-12 16:11:38 +0200 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2025-05-13 15:48:32 +0200 |
| commit | c0b2a8c0f1c565a0343e91e914accafe1e69f3fc (patch) | |
| tree | 0c7179607e59043cc564721b5161ca5fff7af7f2 /android | |
| parent | ce62828a21cb65f2b0d1710b7d92b4e55a518408 (diff) | |
| download | mullvadvpn-c0b2a8c0f1c565a0343e91e914accafe1e69f3fc.tar.xz mullvadvpn-c0b2a8c0f1c565a0343e91e914accafe1e69f3fc.zip | |
Show a specific error if selected port is not in the port range
Diffstat (limited to 'android')
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))) + } + } +} |
