diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/TestMethodButton.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/TestMethodButton.kt index 332cabd6c3e9..9ded39ea1583 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/TestMethodButton.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/button/TestMethodButton.kt @@ -1,82 +1,36 @@ package net.mullvad.mullvadvpn.compose.button -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorSmall import net.mullvad.mullvadvpn.compose.preview.TestMethodButtonPreviewParameterProvider -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState import net.mullvad.mullvadvpn.lib.theme.AppTheme -import net.mullvad.mullvadvpn.lib.theme.Dimens -import net.mullvad.mullvadvpn.lib.theme.color.selected @Preview @Composable private fun PreviewTestMethodButton( - @PreviewParameter(provider = TestMethodButtonPreviewParameterProvider::class) - state: TestApiAccessMethodState? + @PreviewParameter(provider = TestMethodButtonPreviewParameterProvider::class) isTesting: Boolean ) { - AppTheme { TestMethodButton(testMethodState = state, onTestMethod = {}) } + AppTheme { TestMethodButton(isTesting = isTesting, onTestMethod = {}) } } @Composable -fun TestMethodButton( - modifier: Modifier = Modifier, - testMethodState: TestApiAccessMethodState?, - onTestMethod: () -> Unit -) { +fun TestMethodButton(modifier: Modifier = Modifier, isTesting: Boolean, onTestMethod: () -> Unit) { PrimaryButton( modifier = modifier, - leadingIcon = { Icon(testMethodState = testMethodState) }, onClick = onTestMethod, - isEnabled = testMethodState !is TestApiAccessMethodState.Testing, + isEnabled = !isTesting, text = stringResource( id = - when (testMethodState) { - TestApiAccessMethodState.Result.Successful -> R.string.api_reachable - TestApiAccessMethodState.Result.Failure -> R.string.api_unreachable - TestApiAccessMethodState.Testing -> R.string.testing - null -> R.string.test_method + if (isTesting) { + R.string.testing + } else { + R.string.test_method } ), ) } - -@Composable -private fun Icon(testMethodState: TestApiAccessMethodState?) { - when (testMethodState) { - TestApiAccessMethodState.Result.Failure -> - Box( - modifier = - Modifier.padding(start = Dimens.mediumPadding) - .size(Dimens.relayCircleSize) - .background(color = MaterialTheme.colorScheme.error, shape = CircleShape) - ) - TestApiAccessMethodState.Result.Successful -> { - Box( - modifier = - Modifier.padding(start = Dimens.mediumPadding) - .size(Dimens.relayCircleSize) - .background(color = MaterialTheme.colorScheme.selected, shape = CircleShape) - ) - } - TestApiAccessMethodState.Testing -> { - MullvadCircularProgressIndicatorSmall( - modifier = Modifier.padding(start = Dimens.mediumPadding) - ) - } - null -> { - /*Show nothing*/ - } - } -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadExposedDropdownMenuBox.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadExposedDropdownMenuBox.kt index b859670ac18f..060b6f37a9a3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadExposedDropdownMenuBox.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadExposedDropdownMenuBox.kt @@ -42,7 +42,7 @@ fun MullvadExposedDropdownMenuBox( modifier = Modifier.fillMaxWidth().menuAnchor(), readOnly = true, value = title, - onValueChange = { /* Do nothing */ }, + onValueChange = { /* Do nothing */}, label = { Text(text = label) }, trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = colors, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt index 3f614d1c8b9c..c90703b7c4ca 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/SaveApiAccessMethodDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/SaveApiAccessMethodDialog.kt index 1cf4cd0332ab..c0786c3b1ddc 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/SaveApiAccessMethodDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/SaveApiAccessMethodDialog.kt @@ -22,6 +22,7 @@ import net.mullvad.mullvadvpn.compose.button.PrimaryButton import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorMedium import net.mullvad.mullvadvpn.compose.preview.SaveApiAccessMethodUiStatePreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.SaveApiAccessMethodUiState +import net.mullvad.mullvadvpn.compose.state.TestApiAccessMethodState import net.mullvad.mullvadvpn.compose.test.SAVE_API_ACCESS_METHOD_CANCEL_BUTTON_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SAVE_API_ACCESS_METHOD_LOADING_SPINNER_TEST_TAG import net.mullvad.mullvadvpn.compose.test.SAVE_API_ACCESS_METHOD_SAVE_BUTTON_TEST_TAG @@ -29,7 +30,6 @@ import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodType -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.viewmodel.SaveApiAccessMethodSideEffect import net.mullvad.mullvadvpn.viewmodel.SaveApiAccessMethodViewModel diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ApiAccessMethodDetailsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ApiAccessMethodDetailsUiStatePreviewParameterProvider.kt index 5a88ceab5bf3..2f04157967b9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ApiAccessMethodDetailsUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ApiAccessMethodDetailsUiStatePreviewParameterProvider.kt @@ -17,7 +17,7 @@ class ApiAccessMethodDetailsUiStatePreviewParameterProvider : isEditable = false, isCurrentMethod = false, isDisableable = true, - testApiAccessMethodState = null + isTestingAccessMethod = false ) }, // Editable api access type, current method, can not be disabled @@ -29,7 +29,7 @@ class ApiAccessMethodDetailsUiStatePreviewParameterProvider : isEditable = true, isCurrentMethod = true, isDisableable = false, - testApiAccessMethodState = null + isTestingAccessMethod = false ) } ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/EditApiAccessMethodUiStateParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/EditApiAccessMethodUiStateParameterProvider.kt index 726259318692..ae39c2433996 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/EditApiAccessMethodUiStateParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/EditApiAccessMethodUiStateParameterProvider.kt @@ -6,7 +6,6 @@ import net.mullvad.mullvadvpn.compose.state.EditApiAccessFormData import net.mullvad.mullvadvpn.compose.state.EditApiAccessMethodUiState import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodType import net.mullvad.mullvadvpn.lib.model.InvalidDataError -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState class EditApiAccessMethodUiStateParameterProvider : PreviewParameterProvider { @@ -18,7 +17,7 @@ class EditApiAccessMethodUiStateParameterProvider : editMode = false, formData = EditApiAccessFormData.empty(), hasChanges = false, - testMethodState = null + isTestingApiAccessMethod = false ), // Shadowsocks, no errors EditApiAccessMethodUiState.Content( @@ -37,7 +36,7 @@ class EditApiAccessMethodUiStateParameterProvider : username = "" ) }, - testMethodState = null + isTestingApiAccessMethod = false ), // Socks5 Remote, no errors, testing method EditApiAccessMethodUiState.Content( @@ -56,7 +55,7 @@ class EditApiAccessMethodUiStateParameterProvider : password = data.auth?.password ?: "" ) }, - testMethodState = TestApiAccessMethodState.Testing + isTestingApiAccessMethod = true ), // Socks 5 remote, required errors EditApiAccessMethodUiState.Content( @@ -74,7 +73,7 @@ class EditApiAccessMethodUiStateParameterProvider : InvalidDataError.PasswordError.Required ) ), - testMethodState = null + isTestingApiAccessMethod = false ) ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SaveApiAccessMethodUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SaveApiAccessMethodUiStatePreviewParameterProvider.kt index b17f5dec9848..e603d11ea83b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SaveApiAccessMethodUiStatePreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SaveApiAccessMethodUiStatePreviewParameterProvider.kt @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import net.mullvad.mullvadvpn.compose.state.SaveApiAccessMethodUiState -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState +import net.mullvad.mullvadvpn.compose.state.TestApiAccessMethodState class SaveApiAccessMethodUiStatePreviewParameterProvider : PreviewParameterProvider { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TestMethodButtonPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TestMethodButtonPreviewParameterProvider.kt index d9c1ba4ebe0c..1ee6a09c314e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TestMethodButtonPreviewParameterProvider.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TestMethodButtonPreviewParameterProvider.kt @@ -1,15 +1,7 @@ package net.mullvad.mullvadvpn.compose.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState -class TestMethodButtonPreviewParameterProvider : - PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - null, - TestApiAccessMethodState.Testing, - TestApiAccessMethodState.Result.Successful, - TestApiAccessMethodState.Result.Failure - ) +class TestMethodButtonPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf(false, true) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessMethodDetailsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessMethodDetailsScreen.kt index d1d0d4c21624..e71acefe4816 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessMethodDetailsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ApiAccessMethodDetailsScreen.kt @@ -12,9 +12,11 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -100,12 +102,37 @@ fun ApiAccessMethodDetails( navigator.navigate(EditApiAccessMethodDestination(it.apiAccessMethodId)) { launchSingleTop = true } + is ApiAccessMethodDetailsSideEffect.TestApiAccessMethodResult -> { + launch { + snackbarHostState.showSnackbarImmediately( + context.getString( + if (it.successful) { + R.string.api_reachable + } else { + R.string.api_unreachable + } + ) + ) + } + } } } confirmDeleteListResultRecipient.OnNavResultValue { navigator.navigateUp() } val state by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(state.testingAccessMethod()) { + if (state.testingAccessMethod()) { + launch { + snackbarHostState.showSnackbarImmediately( + message = context.getString(R.string.testing), + duration = SnackbarDuration.Indefinite + ) + } + } + } + ApiAccessMethodDetailsScreen( state = state, snackbarHostState = snackbarHostState, @@ -199,12 +226,12 @@ private fun Content( TestMethodButton( modifier = Modifier.padding(horizontal = Dimens.sideMargin).testTag(API_ACCESS_TEST_METHOD_BUTTON), - testMethodState = state.testApiAccessMethodState, + isTesting = state.isTestingAccessMethod, onTestMethod = onTestMethodClicked ) Spacer(modifier = Modifier.height(Dimens.verticalSpace)) PrimaryButton( - isEnabled = !state.isCurrentMethod, + isEnabled = !state.isCurrentMethod && !state.isTestingAccessMethod, modifier = Modifier.padding(horizontal = Dimens.sideMargin).testTag(API_ACCESS_USE_METHOD_BUTTON), onClick = onUseMethodClicked, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditApiAccessMethodScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditApiAccessMethodScreen.kt index 5251bc97113a..23801eb06a4e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditApiAccessMethodScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditApiAccessMethodScreen.kt @@ -10,9 +10,11 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -106,6 +108,20 @@ fun EditApiAccessMethod( ) { launchSingleTop = true } + is EditApiAccessSideEffect.TestApiAccessMethodResult -> { + launch { + snackbarHostState.showSnackbarImmediately( + message = + context.getString( + if (it.successful) { + R.string.api_reachable + } else { + R.string.api_unreachable + } + ) + ) + } + } } } @@ -129,8 +145,21 @@ fun EditApiAccessMethod( } val state by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(state.testingApiAccessMethod()) { + if (state.testingApiAccessMethod()) { + launch { + snackbarHostState.showSnackbarImmediately( + message = context.getString(R.string.testing), + duration = SnackbarDuration.Indefinite + ) + } + } + } + EditApiAccessMethodScreen( state = state, + snackbarHostState = snackbarHostState, onNameChanged = viewModel::onNameChanged, onTypeSelected = viewModel::setAccessMethodType, onIpChanged = viewModel::onServerIpChanged, @@ -227,7 +256,7 @@ fun EditApiAccessMethodScreen( bottom = Dimens.verticalSpace, top = Dimens.largePadding ), - testMethodState = state.testMethodState, + isTesting = state.isTestingApiAccessMethod, onTestMethod = onTestMethod ) AddMethodButton(isNew = !state.editMode, onAddMethod = onAddMethod) @@ -431,6 +460,7 @@ private fun PasswordInput( isValidValue = passwordError == null, isDigitsOnlyAllowed = false, imeAction = + // So that we avoid going back to the name input when pressing done/next if (optional) { ImeAction.Next } else { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ApiAccessMethodDetailsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ApiAccessMethodDetailsUiState.kt index c85d961fd354..3350d1a26c1b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ApiAccessMethodDetailsUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/ApiAccessMethodDetailsUiState.kt @@ -2,7 +2,6 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState sealed interface ApiAccessMethodDetailsUiState { val apiAccessMethodId: ApiAccessMethodId @@ -17,10 +16,12 @@ sealed interface ApiAccessMethodDetailsUiState { val isEditable: Boolean, val isDisableable: Boolean, val isCurrentMethod: Boolean, - val testApiAccessMethodState: TestApiAccessMethodState? + val isTestingAccessMethod: Boolean, ) : ApiAccessMethodDetailsUiState fun name() = (this as? Content)?.name?.value ?: "" - fun canBeEdited() = (this as? Content)?.isEditable ?: false + fun canBeEdited() = this is Content && isEditable + + fun testingAccessMethod() = this is Content && isTestingAccessMethod } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditApiAccessMethodUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditApiAccessMethodUiState.kt index 6716975d0bbe..f111ea2ee4c4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditApiAccessMethodUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditApiAccessMethodUiState.kt @@ -6,7 +6,6 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodType import net.mullvad.mullvadvpn.lib.model.Cipher import net.mullvad.mullvadvpn.lib.model.InvalidDataError -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState sealed interface EditApiAccessMethodUiState { val editMode: Boolean @@ -17,10 +16,12 @@ sealed interface EditApiAccessMethodUiState { override val editMode: Boolean, val formData: EditApiAccessFormData, val hasChanges: Boolean, - val testMethodState: TestApiAccessMethodState? + val isTestingApiAccessMethod: Boolean, ) : EditApiAccessMethodUiState fun hasChanges() = this is Content && hasChanges + + fun testingApiAccessMethod(): Boolean = this is Content && isTestingApiAccessMethod } data class EditApiAccessFormData( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SaveApiAccessMethodUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SaveApiAccessMethodUiState.kt index 7206eedf1327..0ee7b040267b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SaveApiAccessMethodUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SaveApiAccessMethodUiState.kt @@ -1,8 +1,18 @@ package net.mullvad.mullvadvpn.compose.state -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState - data class SaveApiAccessMethodUiState( val testingState: TestApiAccessMethodState = TestApiAccessMethodState.Testing, val isSaving: Boolean = false ) + +sealed interface TestApiAccessMethodState { + data object Testing : TestApiAccessMethodState + + sealed interface Result : TestApiAccessMethodState { + data object Successful : Result + + data object Failure : Result + + fun isSuccessful() = this is Successful + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt index e3373bcee183..320497da62cd 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/test/ComposeTestTagConstants.kt @@ -96,9 +96,6 @@ const val API_ACCESS_LIST_INFO_TEST_TAG = "api_access_list_info_test_tag" // ApiAccessMethodDetailsScreen const val API_ACCESS_DETAILS_TOP_BAR_DROPDOWN_BUTTON_TEST_TAG = "api_access_details_top_bar_dropdown_button_test_tag" -const val API_ACCESS_DETAILS_EDIT_BUTTON = - "api_access_details_edit_button_test_tag" -const val API_ACCESS_USE_METHOD_BUTTON = - "api_access_details_use_method_test_tag" -const val API_ACCESS_TEST_METHOD_BUTTON = - "api_access_details_test_method_test_tag" +const val API_ACCESS_DETAILS_EDIT_BUTTON = "api_access_details_edit_button_test_tag" +const val API_ACCESS_USE_METHOD_BUTTON = "api_access_details_use_method_test_tag" +const val API_ACCESS_TEST_METHOD_BUTTON = "api_access_details_test_method_test_tag" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/ApiAccessMethodTextField.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/ApiAccessMethodTextField.kt index 72bbc0d4a66f..614470da4875 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/ApiAccessMethodTextField.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/ApiAccessMethodTextField.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.compose.textfield import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardOptions @@ -49,7 +50,10 @@ fun ApiAccessMethodTextField( maxCharLength = maxCharLength, supportingText = errorText?.let { { ErrorSupportingText(errorText) } }, colors = apiAccessTextFieldColors(), - modifier = modifier.padding(vertical = Dimens.miniPadding), + modifier = + modifier + .defaultMinSize(minHeight = Dimens.formTextFieldMinHeight) + .padding(vertical = Dimens.miniPadding), keyboardOptions = KeyboardOptions( capitalization = capitalization, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt index 2a5a6a1a871b..95e0130669a3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModel.kt @@ -2,8 +2,8 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.Either import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -11,10 +11,9 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.ApiAccessMethodDetailsUiState -import net.mullvad.mullvadvpn.constant.TEST_METHOD_RESULT_TIME_DURATION import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodType -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState +import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodError import net.mullvad.mullvadvpn.repository.ApiAccessRepository class ApiAccessMethodDetailsViewModel( @@ -23,18 +22,18 @@ class ApiAccessMethodDetailsViewModel( ) : ViewModel() { private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() - private val testApiAccessMethodState = MutableStateFlow(null) + private val isTestingApiAccessMethodState = MutableStateFlow(false) val uiState = combine( apiAccessRepository.apiAccessMethodById(apiAccessMethodId), apiAccessRepository.enabledApiAccessMethods(), apiAccessRepository.currentAccessMethod, - testApiAccessMethodState + isTestingApiAccessMethodState ) { apiAccessMethod, enabledApiAccessMethods, currentAccessMethod, - testApiAccessMethodState -> + isTestingApiAccessMethod -> ApiAccessMethodDetailsUiState.Content( apiAccessMethodId = apiAccessMethodId, name = apiAccessMethod.name, @@ -43,7 +42,7 @@ class ApiAccessMethodDetailsViewModel( apiAccessMethod.apiAccessMethodType is ApiAccessMethodType.CustomProxy, isDisableable = enabledApiAccessMethods.any { it.id != apiAccessMethodId }, isCurrentMethod = currentAccessMethod?.id == apiAccessMethodId, - testApiAccessMethodState = testApiAccessMethodState + isTestingAccessMethod = isTestingApiAccessMethod ) } .stateIn( @@ -54,24 +53,16 @@ class ApiAccessMethodDetailsViewModel( fun setCurrentMethod() { viewModelScope.launch { - apiAccessRepository.setApiAccessMethod(apiAccessMethodId = apiAccessMethodId).onLeft { - _uiSideEffect.send(ApiAccessMethodDetailsSideEffect.GenericError) + testMethodWithStatus().onRight { + apiAccessRepository + .setApiAccessMethod(apiAccessMethodId = apiAccessMethodId) + .onLeft { _uiSideEffect.send(ApiAccessMethodDetailsSideEffect.GenericError) } } } } fun testMethod() { - viewModelScope.launch { - testApiAccessMethodState.value = TestApiAccessMethodState.Testing - apiAccessRepository - .testApiAccessMethodById(apiAccessMethodId) - .fold( - { testApiAccessMethodState.value = TestApiAccessMethodState.Result.Failure }, - { testApiAccessMethodState.value = TestApiAccessMethodState.Result.Successful } - ) - delay(TEST_METHOD_RESULT_TIME_DURATION) - testApiAccessMethodState.value = null - } + viewModelScope.launch { testMethodWithStatus() } } fun setEnableMethod(enable: Boolean) { @@ -87,6 +78,22 @@ class ApiAccessMethodDetailsViewModel( _uiSideEffect.send(ApiAccessMethodDetailsSideEffect.OpenEditPage(apiAccessMethodId)) } } + + private suspend fun testMethodWithStatus(): Either { + isTestingApiAccessMethodState.value = true + return apiAccessRepository + .testApiAccessMethodById(apiAccessMethodId) + .onLeft { + isTestingApiAccessMethodState.value = false + _uiSideEffect.send( + ApiAccessMethodDetailsSideEffect.TestApiAccessMethodResult(false) + ) + } + .onRight { + isTestingApiAccessMethodState.value = false + ApiAccessMethodDetailsSideEffect.TestApiAccessMethodResult(true) + } + } } sealed interface ApiAccessMethodDetailsSideEffect { @@ -94,4 +101,7 @@ sealed interface ApiAccessMethodDetailsSideEffect { ApiAccessMethodDetailsSideEffect data object GenericError : ApiAccessMethodDetailsSideEffect + + data class TestApiAccessMethodResult(val successful: Boolean) : + ApiAccessMethodDetailsSideEffect } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt index 74cf738ae52b..4c84ae1f9f40 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditApiAccessMethodViewModel.kt @@ -12,7 +12,6 @@ import arrow.core.raise.either import arrow.core.raise.ensure import arrow.core.right import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -24,7 +23,6 @@ import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.ApiAccessMethodTypes import net.mullvad.mullvadvpn.compose.state.EditApiAccessFormData import net.mullvad.mullvadvpn.compose.state.EditApiAccessMethodUiState -import net.mullvad.mullvadvpn.constant.TEST_METHOD_RESULT_TIME_DURATION import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodType @@ -33,7 +31,6 @@ import net.mullvad.mullvadvpn.lib.model.InvalidDataError import net.mullvad.mullvadvpn.lib.model.ParsePortError import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.SocksAuth -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState import net.mullvad.mullvadvpn.repository.ApiAccessRepository import org.apache.commons.validator.routines.InetAddressValidator @@ -44,18 +41,18 @@ class EditApiAccessMethodViewModel( ) : ViewModel() { private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() - private val testMethodState = MutableStateFlow(null) + private val isTestingApiAccessMethod = MutableStateFlow(false) private val formData = MutableStateFlow(initialData()) val uiState = - combine(flowOf(initialData()), formData, testMethodState) { + combine(flowOf(initialData()), formData, isTestingApiAccessMethod) { initialData, formData, - testMethodState -> + isTestingApiAccessMethod -> EditApiAccessMethodUiState.Content( editMode = apiAccessMethodId != null, formData = formData, hasChanges = initialData != formData, - testMethodState = testMethodState + isTestingApiAccessMethod = isTestingApiAccessMethod ) } .stateIn( @@ -103,18 +100,22 @@ class EditApiAccessMethodViewModel( .fold( { errors -> formData.update { it.updateWithErrors(errors) } }, { (_, customProxy) -> - testMethodState.value = TestApiAccessMethodState.Testing + isTestingApiAccessMethod.value = true apiAccessRepository .testCustomApiAccessMethod(customProxy) .fold( - { testMethodState.value = TestApiAccessMethodState.Result.Failure }, { - testMethodState.value = - TestApiAccessMethodState.Result.Successful - } + _uiSideEffect.send( + EditApiAccessSideEffect.TestApiAccessMethodResult(false) + ) + }, + { + _uiSideEffect.send( + EditApiAccessSideEffect.TestApiAccessMethodResult(true) + ) + }, ) - delay(TEST_METHOD_RESULT_TIME_DURATION) - testMethodState.value = null + isTestingApiAccessMethod.value = false } ) } @@ -276,4 +277,6 @@ sealed interface EditApiAccessSideEffect { val name: ApiAccessMethodName, val customProxy: ApiAccessMethodType.CustomProxy ) : EditApiAccessSideEffect + + data class TestApiAccessMethodResult(val successful: Boolean) : EditApiAccessSideEffect } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModel.kt index 7a40c410ac41..5b92491cb027 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModel.kt @@ -9,11 +9,11 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.SaveApiAccessMethodUiState +import net.mullvad.mullvadvpn.compose.state.TestApiAccessMethodState import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodType import net.mullvad.mullvadvpn.lib.model.NewAccessMethod -import net.mullvad.mullvadvpn.lib.model.TestApiAccessMethodState import net.mullvad.mullvadvpn.repository.ApiAccessRepository class SaveApiAccessMethodViewModel( diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TestApiAccessMethodState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TestApiAccessMethodState.kt deleted file mode 100644 index c95701d7bef7..000000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TestApiAccessMethodState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.mullvad.mullvadvpn.lib.model - -sealed interface TestApiAccessMethodState { - data object Testing : TestApiAccessMethodState - - sealed interface Result : TestApiAccessMethodState { - data object Successful : Result - - data object Failure : Result - - fun isSuccessful() = this is Successful - } -} diff --git a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/color/Color.kt b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/color/Color.kt index 7dffcdd4cf9f..343e41dc1a0f 100644 --- a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/color/Color.kt +++ b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/color/Color.kt @@ -44,6 +44,3 @@ val menuItemColors: MenuItemColors leadingIconColor = MaterialTheme.colorScheme.onSurface, textColor = MaterialTheme.colorScheme.onSurface, ) - - - diff --git a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt index 2763033a306d..ef3564951f8f 100644 --- a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt +++ b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt @@ -42,6 +42,7 @@ data class Dimensions( val dropdownMenuBorder: Dp = 1.dp, val expandableCellChevronSize: Dp = 30.dp, val filterTittlePadding: Dp = 4.dp, + val formTextFieldMinHeight: Dp = 72.dp, val iconFailSuccessTopMargin: Dp = 30.dp, val iconHeight: Dp = 44.dp, val indentedCellStartPadding: Dp = 38.dp,