From 36632d45e320ad7667083893e38234dde1b4f42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20G=C3=B6ransson?= Date: Mon, 24 Jun 2024 11:07:48 +0200 Subject: [PATCH] Use NavArgs & SavedStateHandle --- .../DeleteCustomListConfirmationDialogTest.kt | 9 ++-- .../dialog/EditCustomListNameDialogTest.kt | 4 +- .../screen/EditCustomListScreenTest.kt | 4 +- .../compose/dialog/CreateCustomListDialog.kt | 12 +++-- ...DeleteApiAccessMethodConfirmationDialog.kt | 14 +++--- .../DeleteCustomListConfirmationDialog.kt | 28 +++++------ .../mullvadvpn/compose/dialog/DnsDialog.kt | 13 +++--- .../dialog/EditCustomListNameDialog.kt | 39 +++++++--------- .../mullvadvpn/compose/dialog/MtuDialog.kt | 11 +++-- .../dialog/SaveApiAccessMethodDialog.kt | 20 ++++---- .../screen/ApiAccessMethodDetailsScreen.kt | 14 +++--- .../screen/CustomListLocationsScreen.kt | 18 ++++---- .../compose/screen/CustomListsScreen.kt | 2 +- .../compose/screen/DeviceListScreen.kt | 24 ++++------ .../screen/EditApiAccessMethodScreen.kt | 12 +++-- .../compose/screen/EditCustomListScreen.kt | 36 +++++++++------ .../mullvadvpn/compose/screen/LoginScreen.kt | 2 +- .../compose/state/DeleteCustomListUiState.kt | 3 +- .../state/EditCustomListNameUiState.kt | 4 +- .../net/mullvad/mullvadvpn/di/UiModule.kt | 46 +++++-------------- .../ApiAccessMethodDetailsViewModel.kt | 9 +++- .../CreateCustomListDialogViewModel.kt | 7 ++- .../viewmodel/CustomListLocationsViewModel.kt | 24 +++++----- ...eteApiAccessMethodConfirmationViewModel.kt | 9 +++- .../DeleteCustomListConfirmationViewModel.kt | 15 ++++-- .../viewmodel/DeviceListViewModel.kt | 22 +++++++-- .../viewmodel/DnsDialogViewModel.kt | 10 ++-- .../viewmodel/EditApiAccessMethodViewModel.kt | 8 +++- .../EditCustomListNameDialogViewModel.kt | 25 +++++----- .../viewmodel/EditCustomListViewModel.kt | 9 +++- .../viewmodel/MtuDialogViewModel.kt | 10 ++-- .../viewmodel/SaveApiAccessMethodViewModel.kt | 13 ++++-- .../ApiAccessMethodDetailsViewModelTest.kt | 8 +++- .../CreateCustomListDialogViewModelTest.kt | 10 +++- .../CustomListLocationsViewModelTest.kt | 12 +++-- ...piAccessMethodConfirmationViewModelTest.kt | 8 +++- ...leteCustomListConfirmationViewModelTest.kt | 12 ++++- .../EditCustomListNameDialogViewModelTest.kt | 15 +++--- .../viewmodel/EditCustomListViewModelTest.kt | 30 ++++++------ .../SaveApiAccessMethodViewModelTest.kt | 14 ++++-- android/config/baseline.xml | 4 +- .../mullvadvpn/lib/model/RelayItemId.kt | 3 +- 42 files changed, 337 insertions(+), 255 deletions(-) diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt index ee347c246a0a..184cd7defe5e 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialogTest.kt @@ -32,8 +32,7 @@ class DeleteCustomListConfirmationDialogTest { val name = CustomListName.fromString("List should be deleted") setContentWithTheme { DeleteCustomListConfirmationDialog( - name = name, - state = DeleteCustomListUiState(null) + state = DeleteCustomListUiState(name = name, deleteError = null), ) } @@ -49,8 +48,7 @@ class DeleteCustomListConfirmationDialogTest { val mockedOnDelete: () -> Unit = mockk(relaxed = true) setContentWithTheme { DeleteCustomListConfirmationDialog( - name = name, - state = DeleteCustomListUiState(null), + state = DeleteCustomListUiState(name = name, deleteError = null), onDelete = mockedOnDelete ) } @@ -70,8 +68,7 @@ class DeleteCustomListConfirmationDialogTest { val mockedOnBack: () -> Unit = mockk(relaxed = true) setContentWithTheme { DeleteCustomListConfirmationDialog( - name = name, - state = DeleteCustomListUiState(null), + state = DeleteCustomListUiState(name = name, deleteError = null), onBack = mockedOnBack ) } diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt index 3128bbc508f1..6bca03db695c 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialogTest.kt @@ -126,7 +126,7 @@ class EditCustomListNameDialogTest { fun whenInputIsChangedShouldCallOnInputChanged() = composeExtension.use { // Arrange - val mockedOnInputChanged: () -> Unit = mockk(relaxed = true) + val mockedOnInputChanged: (String) -> Unit = mockk(relaxed = true) val inputText = "NEW NAME" val state = EditCustomListNameUiState() setContentWithTheme { @@ -137,7 +137,7 @@ class EditCustomListNameDialogTest { onNodeWithTag(EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG).performTextInput(inputText) // Assert - verify { mockedOnInputChanged.invoke() } + verify { mockedOnInputChanged.invoke(inputText) } } companion object { diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt index 5e5730977766..2bc53013ca40 100644 --- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt +++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreenTest.kt @@ -93,7 +93,7 @@ class EditCustomListScreenTest { fun whenClickingOnDeleteDropdownShouldCallOnDeleteList() = composeExtension.use { // Arrange - val mockedOnDelete: (CustomListName) -> Unit = mockk(relaxed = true) + val mockedOnDelete: (CustomListId, CustomListName) -> Unit = mockk(relaxed = true) val customList = DUMMY_CUSTOM_LISTS[0] setContentWithTheme { EditCustomListScreen( @@ -112,7 +112,7 @@ class EditCustomListScreenTest { onNodeWithTag(DELETE_DROPDOWN_MENU_ITEM_TEST_TAG).performClick() // Assert - verify { mockedOnDelete(customList.name) } + verify { mockedOnDelete(customList.id, customList.name) } } @Test diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt index 9e9cb5b78d61..db7f664e37b4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt @@ -34,7 +34,6 @@ import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError import net.mullvad.mullvadvpn.viewmodel.CreateCustomListDialogSideEffect import net.mullvad.mullvadvpn.viewmodel.CreateCustomListDialogViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -55,15 +54,18 @@ private fun PreviewCreateCustomListDialogError() { } } +data class CreateCustomListNavArgs(val locationCode: GeoLocationId?) + @Composable -@Destination(style = DestinationStyle.Dialog::class) +@Destination( + style = DestinationStyle.Dialog::class, + navArgs = CreateCustomListNavArgs::class +) fun CreateCustomList( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator, - locationCode: GeoLocationId? = null ) { - val vm: CreateCustomListDialogViewModel = - koinViewModel(parameters = { parametersOf(locationCode) }) + val vm: CreateCustomListDialogViewModel = koinViewModel() LaunchedEffect(key1 = Unit) { vm.uiSideEffect.collect { sideEffect -> when (sideEffect) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteApiAccessMethodConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteApiAccessMethodConfirmationDialog.kt index 021a78e5eb09..6472f7d1b2b1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteApiAccessMethodConfirmationDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteApiAccessMethodConfirmationDialog.kt @@ -16,7 +16,6 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.viewmodel.DeleteApiAccessMethodConfirmationSideEffect import net.mullvad.mullvadvpn.viewmodel.DeleteApiAccessMethodConfirmationViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -24,16 +23,17 @@ private fun PreviewDeleteApiAccessMethodConfirmationDialog() { AppTheme { DeleteApiAccessMethodConfirmationDialog(state = DeleteApiAccessMethodUiState(null)) } } +data class DeleteApiAccessMethodNavArgs(val apiAccessMethodId: ApiAccessMethodId) + @Composable -@Destination(style = DestinationStyle.Dialog::class) +@Destination( + style = DestinationStyle.Dialog::class, + navArgs = DeleteApiAccessMethodNavArgs::class +) fun DeleteApiAccessMethodConfirmation( navigator: ResultBackNavigator, - apiAccessMethodId: ApiAccessMethodId ) { - val viewModel = - koinViewModel( - parameters = { parametersOf(apiAccessMethodId) } - ) + val viewModel = koinViewModel() val state = viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffectCollect(viewModel.uiSideEffect) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt index 22598d3d2d66..0f26bcbe4865 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt @@ -1,6 +1,7 @@ package net.mullvad.mullvadvpn.compose.dialog import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -19,29 +20,29 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.viewmodel.DeleteCustomListConfirmationSideEffect import net.mullvad.mullvadvpn.viewmodel.DeleteCustomListConfirmationViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable private fun PreviewRemoveDeviceConfirmationDialog() { AppTheme { DeleteCustomListConfirmationDialog( - state = DeleteCustomListUiState(null), - name = CustomListName.fromString("My Custom List") + state = DeleteCustomListUiState(CustomListName.fromString("My Custom List"), null) ) } } +data class DeleteCustomListNavArgs(val customListId: CustomListId, val name: CustomListName) + @Composable -@Destination(style = DestinationStyle.Dialog::class) +@Destination( + style = DestinationStyle.Dialog::class, + navArgs = DeleteCustomListNavArgs::class +) fun DeleteCustomList( navigator: ResultBackNavigator, - customListId: CustomListId, - name: CustomListName ) { - val viewModel: DeleteCustomListConfirmationViewModel = - koinViewModel(parameters = { parametersOf(customListId) }) - val state = viewModel.uiState.collectAsStateWithLifecycle() + val viewModel: DeleteCustomListConfirmationViewModel = koinViewModel() + val state by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffectCollect(viewModel.uiSideEffect) { when (it) { @@ -51,8 +52,7 @@ fun DeleteCustomList( } DeleteCustomListConfirmationDialog( - state = state.value, - name = name, + state = state, onDelete = viewModel::deleteCustomList, onBack = dropUnlessResumed { navigator.navigateBack() } ) @@ -61,7 +61,6 @@ fun DeleteCustomList( @Composable fun DeleteCustomListConfirmationDialog( state: DeleteCustomListUiState, - name: CustomListName, onDelete: () -> Unit = {}, onBack: () -> Unit = {} ) { @@ -69,7 +68,10 @@ fun DeleteCustomListConfirmationDialog( onDelete = onDelete, onBack = onBack, message = - stringResource(id = R.string.delete_custom_list_confirmation_description, name.value), + stringResource( + id = R.string.delete_custom_list_confirmation_description, + state.name.value + ), errorMessage = if (state.deleteError != null) { stringResource(id = R.string.error_occurred) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt index 8db8caffe567..518539dd6453 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DnsDialog.kt @@ -32,7 +32,6 @@ import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewModel import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewState import net.mullvad.mullvadvpn.viewmodel.ValidationError import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -52,15 +51,17 @@ private fun PreviewDnsDialogEditAllowLanDisabled() { AppTheme { DnsDialog(DnsDialogViewState("192.168.1.1", null, true, false, 0), {}, {}, {}, {}) } } -@Destination(style = DestinationStyle.Dialog::class) +data class DnsDialogNavArgs( + val index: Int? = null, + val initialValue: String? = null, +) + +@Destination(style = DestinationStyle.Dialog::class, navArgs = DnsDialogNavArgs::class) @Composable fun DnsDialog( resultNavigator: ResultBackNavigator, - index: Int?, - initialValue: String?, ) { - val viewModel = - koinViewModel(parameters = { parametersOf(initialValue, index) }) + val viewModel = koinViewModel() LaunchedEffectCollect(viewModel.uiSideEffect) { when (it) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt index 42fe1cbf82eb..3a6d234fc6b1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt @@ -4,10 +4,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -35,7 +32,6 @@ import net.mullvad.mullvadvpn.usecase.customlists.RenameError import net.mullvad.mullvadvpn.viewmodel.EditCustomListNameDialogSideEffect import net.mullvad.mullvadvpn.viewmodel.EditCustomListNameDialogViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -43,15 +39,20 @@ private fun PreviewEditCustomListNameDialog() { AppTheme { EditCustomListNameDialog(EditCustomListNameUiState()) } } +data class EditCustomListNameNavArgs( + val customListId: CustomListId, + val initialName: CustomListName +) + @Composable -@Destination(style = DestinationStyle.Dialog::class) +@Destination( + style = DestinationStyle.Dialog::class, + navArgs = EditCustomListNameNavArgs::class +) fun EditCustomListName( backNavigator: ResultBackNavigator, - customListId: CustomListId, - initialName: CustomListName ) { - val vm: EditCustomListNameDialogViewModel = - koinViewModel(parameters = { parametersOf(customListId, initialName) }) + val vm: EditCustomListNameDialogViewModel = koinViewModel() LaunchedEffectCollect(vm.uiSideEffect) { sideEffect -> when (sideEffect) { is EditCustomListNameDialogSideEffect.ReturnWithResult -> { @@ -64,7 +65,7 @@ fun EditCustomListName( EditCustomListNameDialog( state = state, updateName = vm::updateCustomListName, - onInputChanged = vm::clearError, + onInputChanged = vm::onNameChanged, onDismiss = dropUnlessResumed { backNavigator.navigateBack() } ) } @@ -73,12 +74,9 @@ fun EditCustomListName( fun EditCustomListNameDialog( state: EditCustomListNameUiState, updateName: (String) -> Unit = {}, - onInputChanged: () -> Unit = {}, + onInputChanged: (String) -> Unit = {}, onDismiss: () -> Unit = {} ) { - val name = remember { mutableStateOf(state.name) } - val isValidName by remember { derivedStateOf { name.value.isNotBlank() } } - AlertDialog( title = { Text( @@ -87,14 +85,11 @@ fun EditCustomListNameDialog( }, text = { CustomListNameTextField( - name = name.value, - isValidName = isValidName, + name = state.name, + isValidName = state.isValidName, error = state.error?.errorString(), onSubmit = updateName, - onValueChanged = { - name.value = it - onInputChanged() - }, + onValueChanged = onInputChanged, modifier = Modifier.testTag(EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG) ) }, @@ -104,8 +99,8 @@ fun EditCustomListNameDialog( confirmButton = { PrimaryButton( text = stringResource(id = R.string.save), - onClick = { updateName(name.value) }, - isEnabled = isValidName + onClick = { updateName(state.name) }, + isEnabled = state.isValidName ) }, dismissButton = { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt index e3d44c0fd5ac..f90182640dbb 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/MtuDialog.kt @@ -34,18 +34,19 @@ import net.mullvad.mullvadvpn.viewmodel.MtuDialogSideEffect import net.mullvad.mullvadvpn.viewmodel.MtuDialogUiState import net.mullvad.mullvadvpn.viewmodel.MtuDialogViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable private fun PreviewMtuDialog() { - AppTheme { MtuDialog(mtuInitial = Mtu(1234), EmptyResultBackNavigator()) } + AppTheme { MtuDialog(EmptyResultBackNavigator()) } } -@Destination(style = DestinationStyle.Dialog::class) +data class MtuNavArgs(val initialMtu: Mtu? = null) + +@Destination(style = DestinationStyle.Dialog::class, navArgs = MtuNavArgs::class) @Composable -fun MtuDialog(mtuInitial: Mtu?, navigator: ResultBackNavigator) { - val viewModel = koinViewModel(parameters = { parametersOf(mtuInitial) }) +fun MtuDialog(navigator: ResultBackNavigator) { + val viewModel = koinViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffectCollect(viewModel.uiSideEffect) { 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 19f02d4802e2..be13b62decde 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 @@ -35,7 +35,6 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.viewmodel.SaveApiAccessMethodSideEffect import net.mullvad.mullvadvpn.viewmodel.SaveApiAccessMethodViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -46,18 +45,21 @@ private fun PreviewSaveApiAccessMethodDialog( AppTheme { SaveApiAccessMethodDialog(state = state) } } -@Destination(style = DestinationStyle.Dialog::class) +data class SaveApiAccessMethodNavArgs( + val id: ApiAccessMethodId?, + val name: ApiAccessMethodName, + val customProxy: ApiAccessMethod.CustomProxy +) + +@Destination( + style = DestinationStyle.Dialog::class, + navArgs = SaveApiAccessMethodNavArgs::class +) @Composable fun SaveApiAccessMethod( backNavigator: ResultBackNavigator, - id: ApiAccessMethodId?, - name: ApiAccessMethodName, - customProxy: ApiAccessMethod.CustomProxy ) { - val viewModel = - koinViewModel( - parameters = { parametersOf(id, name, customProxy) } - ) + val viewModel = koinViewModel() LaunchedEffectCollect(sideEffect = viewModel.uiSideEffect) { when (it) { 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 d5aa7dfa0e90..f876a0790fac 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 @@ -65,7 +65,6 @@ import net.mullvad.mullvadvpn.lib.theme.color.menuItemColors import net.mullvad.mullvadvpn.viewmodel.ApiAccessMethodDetailsSideEffect import net.mullvad.mullvadvpn.viewmodel.ApiAccessMethodDetailsViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -76,18 +75,19 @@ private fun PreviewApiAccessMethodDetailsScreen( AppTheme { ApiAccessMethodDetailsScreen(state = state) } } -@Destination(style = SlideInFromRightTransition::class) +data class ApiAccessMethodDetailsNavArgs(val accessMethodId: ApiAccessMethodId) + +@Destination( + style = SlideInFromRightTransition::class, + navArgs = ApiAccessMethodDetailsNavArgs::class +) @Composable fun ApiAccessMethodDetails( navigator: DestinationsNavigator, - accessMethodId: ApiAccessMethodId, confirmDeleteListResultRecipient: ResultRecipient ) { - val viewModel = - koinViewModel( - parameters = { parametersOf(accessMethodId) } - ) + val viewModel = koinViewModel() val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt index 4d60c06b0a4e..9350b8776e8a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt @@ -60,7 +60,6 @@ import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsSideEffect import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Composable @Preview @@ -68,19 +67,22 @@ private fun PreviewCustomListLocationScreen() { AppTheme { CustomListLocationsScreen(state = CustomListLocationsUiState.Content.Data()) } } +data class CustomListLocationsNavArgs( + val customListId: CustomListId, + val newList: Boolean, +) + @Composable -@Destination(style = SlideInFromRightTransition::class) +@Destination( + style = SlideInFromRightTransition::class, + navArgs = CustomListLocationsNavArgs::class +) fun CustomListLocations( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator, discardChangesResultRecipient: ResultRecipient, - customListId: CustomListId, - newList: Boolean, ) { - val customListsViewModel = - koinViewModel( - parameters = { parametersOf(customListId, newList) } - ) + val customListsViewModel = koinViewModel() discardChangesResultRecipient.onNavResult { when (it) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt index 8cdef9bd7685..5b9f0d4cc1c1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt @@ -98,7 +98,7 @@ fun CustomLists( addCustomList = dropUnlessResumed { navigator.navigate( - CreateCustomListDestination(), + CreateCustomListDestination(null), ) }, openCustomList = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt index 1ea7d9e80c91..74d4714d6de9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceListScreen.kt @@ -70,7 +70,6 @@ import net.mullvad.mullvadvpn.util.formatDate import net.mullvad.mullvadvpn.viewmodel.DeviceListSideEffect import net.mullvad.mullvadvpn.viewmodel.DeviceListViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Composable @Preview @@ -103,18 +102,16 @@ private fun PreviewDeviceListError() { } } -@Destination(style = DefaultTransition::class) +data class DeviceListNavArgs(val accountNumber: AccountNumber) + +@Destination(style = DefaultTransition::class, navArgs = DeviceListNavArgs::class) @Composable fun DeviceList( navigator: DestinationsNavigator, - accountNumber: String, confirmRemoveResultRecipient: ResultRecipient ) { - val viewModel = - koinViewModel( - parameters = { parametersOf(AccountNumber(accountNumber)) } - ) + val viewModel = koinViewModel() val state by viewModel.uiState.collectAsStateWithLifecycle() confirmRemoveResultRecipient.onNavResult { @@ -142,6 +139,11 @@ fun DeviceList( ) } } + is DeviceListSideEffect.NavigateToLogin -> + navigator.navigate(LoginDestination(sideEffect.accountNumber.value)) { + launchSingleTop = true + popUpTo(LoginDestination) { inclusive = true } + } } } @@ -149,13 +151,7 @@ fun DeviceList( state = state, snackbarHostState = snackbarHostState, onBackClick = dropUnlessResumed { navigator.navigateUp() }, - onContinueWithLogin = - dropUnlessResumed { - navigator.navigate(LoginDestination(accountNumber)) { - launchSingleTop = true - popUpTo(LoginDestination) { inclusive = true } - } - }, + onContinueWithLogin = viewModel::continueToLogin, onSettingsClicked = dropUnlessResumed { navigator.navigate(SettingsDestination) }, onTryAgainClicked = viewModel::fetchDevices, navigateToRemoveDeviceConfirmationDialog = 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 3d2926b90cc2..84162e501eb7 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 @@ -72,7 +72,6 @@ import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible import net.mullvad.mullvadvpn.viewmodel.EditApiAccessMethodViewModel import net.mullvad.mullvadvpn.viewmodel.EditApiAccessSideEffect import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -83,17 +82,20 @@ private fun PreviewEditApiAccessMethodScreen( AppTheme { EditApiAccessMethodScreen(state = state) } } -@Destination(style = SlideInFromRightTransition::class) +data class EditApiAccessMethodNavArgs(val accessMethodId: ApiAccessMethodId?) + +@Destination( + style = SlideInFromRightTransition::class, + navArgs = EditApiAccessMethodNavArgs::class +) @Composable fun EditApiAccessMethod( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator, saveApiAccessMethodResultRecipient: ResultRecipient, discardChangesResultRecipient: ResultRecipient, - accessMethodId: ApiAccessMethodId? ) { - val viewModel = - koinViewModel(parameters = { parametersOf(accessMethodId) }) + val viewModel = koinViewModel() val snackbarHostState = remember { SnackbarHostState() } val context = LocalContext.current diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt index 341c8e9f1699..c3f7662ea250 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt @@ -52,7 +52,6 @@ import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.menuItemColors import net.mullvad.mullvadvpn.viewmodel.EditCustomListViewModel import org.koin.androidx.compose.koinViewModel -import org.koin.core.parameter.parametersOf @Preview @Composable @@ -78,16 +77,19 @@ private fun PreviewEditCustomListScreen() { } } +data class EditCustomListNavArgs(val customListId: CustomListId) + @Composable -@Destination(style = SlideInFromRightTransition::class) +@Destination( + style = SlideInFromRightTransition::class, + navArgs = EditCustomListNavArgs::class +) fun EditCustomList( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator, - customListId: CustomListId, confirmDeleteListResultRecipient: ResultRecipient ) { - val viewModel = - koinViewModel(parameters = { parametersOf(customListId) }) + val viewModel = koinViewModel() confirmDeleteListResultRecipient.onNavResult { when (it) { @@ -103,9 +105,9 @@ fun EditCustomList( EditCustomListScreen( state = state, onDeleteList = - dropUnlessResumed { name -> + dropUnlessResumed { id, name -> navigator.navigate( - DeleteCustomListDestination(customListId = customListId, name = name), + DeleteCustomListDestination(customListId = id, name = name), ) }, onNameClicked = @@ -127,21 +129,25 @@ fun EditCustomList( @Composable fun EditCustomListScreen( state: EditCustomListState, - onDeleteList: (name: CustomListName) -> Unit = {}, + onDeleteList: (id: CustomListId, name: CustomListName) -> Unit = { _, _ -> }, onNameClicked: (id: CustomListId, name: CustomListName) -> Unit = { _, _ -> }, onLocationsClicked: (CustomListId) -> Unit = {}, onBackClick: () -> Unit = {} ) { - val title = - when (state) { - EditCustomListState.Loading, - EditCustomListState.NotFound -> null - is EditCustomListState.Content -> state.name - } ScaffoldWithMediumTopBar( appBarTitle = stringResource(id = R.string.edit_list), navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) }, - actions = { Actions(enabled = title != null, onDeleteList = { onDeleteList(title!!) }) }, + actions = { + val content = state as? EditCustomListState.Content + Actions( + enabled = content?.name != null, + onDeleteList = { + if (content is EditCustomListState.Content) { + onDeleteList(content.id, content.name) + } + } + ) + }, ) { modifier: Modifier -> SpacedColumn(modifier = modifier, alignment = Alignment.Top) { when (state) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt index 3cb51614c2b7..88d5ef57f290 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt @@ -141,7 +141,7 @@ fun Login( popUpTo(NavGraphs.root) { inclusive = true } } is LoginUiSideEffect.TooManyDevices -> - navigator.navigate(DeviceListDestination(it.accountNumber.value)) { + navigator.navigate(DeviceListDestination(it.accountNumber)) { launchSingleTop = true } LoginUiSideEffect.NavigateToOutOfTime -> diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt index 000fc13f4aa0..75b4cd6aaf49 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeleteCustomListUiState.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.compose.state +import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.usecase.customlists.DeleteWithUndoError -data class DeleteCustomListUiState(val deleteError: DeleteWithUndoError?) +data class DeleteCustomListUiState(val name: CustomListName, val deleteError: DeleteWithUndoError?) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt index 9e6bcdecf8c5..06e9fc709b86 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/EditCustomListNameUiState.kt @@ -2,4 +2,6 @@ package net.mullvad.mullvadvpn.compose.state import net.mullvad.mullvadvpn.usecase.customlists.RenameError -data class EditCustomListNameUiState(val name: String = "", val error: RenameError? = null) +data class EditCustomListNameUiState(val name: String = "", val error: RenameError? = null) { + val isValidName = name.isNotBlank() +} 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 d752460ddbf2..ec940364dc51 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 @@ -9,10 +9,6 @@ import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.applist.ApplicationsProvider import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport -import net.mullvad.mullvadvpn.lib.model.ApiAccessMethod -import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId -import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName -import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.payment.PaymentProvider import net.mullvad.mullvadvpn.lib.shared.VoucherRepository import net.mullvad.mullvadvpn.repository.ApiAccessRepository @@ -178,12 +174,10 @@ val uiModule = module { IS_PLAY_BUILD ) } - viewModel { parameters -> DeviceListViewModel(get(), parameters.get()) } + viewModel { DeviceListViewModel(get(), get()) } viewModel { DeviceRevokedViewModel(get(), get()) } - viewModel { parameters -> MtuDialogViewModel(get(), parameters.getOrNull()) } - viewModel { parameters -> - DnsDialogViewModel(get(), get(), parameters.getOrNull(), parameters.getOrNull()) - } + viewModel { MtuDialogViewModel(get(), get()) } + viewModel { DnsDialogViewModel(get(), get(), get()) } viewModel { LoginViewModel(get(), get(), get()) } viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) } viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get()) } @@ -197,36 +191,20 @@ val uiModule = module { viewModel { OutOfTimeViewModel(get(), get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) } viewModel { PaymentViewModel(get()) } viewModel { FilterViewModel(get(), get()) } - viewModel { (location: GeoLocationId?) -> CreateCustomListDialogViewModel(location, get()) } - viewModel { parameters -> - CustomListLocationsViewModel(parameters.get(), parameters.get(), get(), get(), get()) - } - viewModel { parameters -> EditCustomListViewModel(parameters.get(), get()) } - viewModel { parameters -> - EditCustomListNameDialogViewModel(parameters.get(), parameters.get(), get()) - } + viewModel { CreateCustomListDialogViewModel(get(), get()) } + viewModel { CustomListLocationsViewModel(get(), get(), get(), get()) } + viewModel { EditCustomListViewModel(get(), get()) } + viewModel { EditCustomListNameDialogViewModel(get(), get()) } viewModel { CustomListsViewModel(get(), get()) } - viewModel { parameters -> DeleteCustomListConfirmationViewModel(parameters.get(), get()) } + viewModel { DeleteCustomListConfirmationViewModel(get(), get()) } viewModel { ServerIpOverridesViewModel(get(), get()) } viewModel { ResetServerIpOverridesConfirmationViewModel(get()) } viewModel { VpnPermissionViewModel(get(), get()) } viewModel { ApiAccessListViewModel(get()) } - viewModel { (accessMethodId: ApiAccessMethodId?) -> - EditApiAccessMethodViewModel(accessMethodId, get(), get()) - } - viewModel { - ( - id: ApiAccessMethodId?, - name: ApiAccessMethodName, - customProxy: ApiAccessMethod.CustomProxy) -> - SaveApiAccessMethodViewModel(id, name, customProxy, get()) - } - viewModel { (accessMethodId: ApiAccessMethodId) -> - ApiAccessMethodDetailsViewModel(accessMethodId, get()) - } - viewModel { (accessMethodId: ApiAccessMethodId) -> - DeleteApiAccessMethodConfirmationViewModel(accessMethodId, get()) - } + viewModel { EditApiAccessMethodViewModel(get(), get(), get()) } + viewModel { SaveApiAccessMethodViewModel(get(), get()) } + viewModel { ApiAccessMethodDetailsViewModel(get(), get()) } + viewModel { DeleteApiAccessMethodConfirmationViewModel(get(), get()) } // This view model must be single so we correctly attach lifecycle and share it with activity single { NoDaemonViewModel(get()) } 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 fc52cb7eeefb..f133cf217268 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 @@ -1,9 +1,11 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.Either import arrow.core.raise.either +import com.ramcosta.composedestinations.generated.destinations.ApiAccessMethodDetailsDestination import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel @@ -22,11 +24,14 @@ import net.mullvad.mullvadvpn.repository.ApiAccessRepository import net.mullvad.mullvadvpn.util.delayAtLeast class ApiAccessMethodDetailsViewModel( - private val apiAccessMethodId: ApiAccessMethodId, - private val apiAccessRepository: ApiAccessRepository + private val apiAccessRepository: ApiAccessRepository, + savedStateHandle: SavedStateHandle ) : ViewModel() { private var testingJob: Job? = null + private val apiAccessMethodId: ApiAccessMethodId = + ApiAccessMethodDetailsDestination.argsFrom(savedStateHandle).accessMethodId + private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() private val isTestingApiAccessMethodState = MutableStateFlow(false) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt index ef453029750b..e367386bf265 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.CreateCustomListDestination import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -20,10 +22,13 @@ import net.mullvad.mullvadvpn.usecase.customlists.CreateWithLocationsError import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase class CreateCustomListDialogViewModel( - private val locationCode: GeoLocationId?, private val customListActionUseCase: CustomListActionUseCase, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val locationCode: GeoLocationId? = + CreateCustomListDestination.argsFrom(savedStateHandle).locationCode + private val _uiSideEffect = Channel(1, BufferOverflow.DROP_OLDEST) val uiSideEffect = _uiSideEffect.receiveAsFlow() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt index f25384c1207f..6d738a641707 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.CustomListLocationsDestination import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow @@ -14,7 +16,6 @@ import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.LocationsChanged import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState -import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.relaylist.descendants import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm @@ -24,12 +25,13 @@ import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase class CustomListLocationsViewModel( - private val customListId: CustomListId, - private val newList: Boolean, relayListRepository: RelayListRepository, private val customListRelayItemsUseCase: CustomListRelayItemsUseCase, - private val customListActionUseCase: CustomListActionUseCase + private val customListActionUseCase: CustomListActionUseCase, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val navArgs = + CustomListLocationsDestination.argsFrom(savedStateHandle = savedStateHandle) private val _uiSideEffect = MutableSharedFlow(replay = 1, extraBufferCapacity = 1) val uiSideEffect: SharedFlow = _uiSideEffect @@ -47,15 +49,15 @@ class CustomListLocationsViewModel( when { selectedLocations == null -> - CustomListLocationsUiState.Loading(newList = newList) + CustomListLocationsUiState.Loading(newList = navArgs.newList) filteredRelayCountries.isEmpty() -> CustomListLocationsUiState.Content.Empty( - newList = newList, + newList = navArgs.newList, searchTerm = searchTerm ) else -> CustomListLocationsUiState.Content.Data( - newList = newList, + newList = navArgs.newList, searchTerm = searchTerm, availableLocations = filteredRelayCountries, selectedLocations = selectedLocations, @@ -69,7 +71,7 @@ class CustomListLocationsViewModel( .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - CustomListLocationsUiState.Loading(newList = newList) + CustomListLocationsUiState.Loading(newList = navArgs.newList) ) init { @@ -81,7 +83,7 @@ class CustomListLocationsViewModel( _selectedLocations.value?.let { selectedLocations -> customListActionUseCase( CustomListAction.UpdateLocations( - customListId, + navArgs.customListId, selectedLocations.calculateLocationsToSave().map { it.id } ) ) @@ -91,7 +93,7 @@ class CustomListLocationsViewModel( _uiSideEffect.tryEmit( // This is so that we don't show a snackbar after returning to the // select location screen - if (newList) { + if (navArgs.newList) { CustomListLocationsSideEffect.CloseScreen } else { CustomListLocationsSideEffect.ReturnWithResult(it) @@ -190,7 +192,7 @@ class CustomListLocationsViewModel( private suspend fun fetchInitialSelectedLocations() { val selectedLocations = - customListRelayItemsUseCase(customListId).first().withDescendants().toSet() + customListRelayItemsUseCase(navArgs.customListId).first().withDescendants().toSet() _initialLocations.value = selectedLocations _selectedLocations.value = selectedLocations diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt index 651081244f98..bac2e3ad95e5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.DeleteApiAccessMethodConfirmationDestination import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -15,9 +17,12 @@ import net.mullvad.mullvadvpn.lib.model.RemoveApiAccessMethodError import net.mullvad.mullvadvpn.repository.ApiAccessRepository class DeleteApiAccessMethodConfirmationViewModel( - private val apiAccessMethodId: ApiAccessMethodId, - private val apiAccessRepository: ApiAccessRepository + private val apiAccessRepository: ApiAccessRepository, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val apiAccessMethodId: ApiAccessMethodId = + DeleteApiAccessMethodConfirmationDestination.argsFrom(savedStateHandle).apiAccessMethodId + private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt index 8bea330f83cb..c492bd368a30 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.DeleteCustomListDestination import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -13,13 +15,18 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.Deleted import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState import net.mullvad.mullvadvpn.lib.model.CustomListId +import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.DeleteWithUndoError class DeleteCustomListConfirmationViewModel( - private val customListId: CustomListId, - private val customListActionUseCase: CustomListActionUseCase + private val customListActionUseCase: CustomListActionUseCase, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val navArgs = DeleteCustomListDestination.argsFrom(savedStateHandle) + private val name: CustomListName = navArgs.name + private val customListId: CustomListId = navArgs.customListId + private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() @@ -27,11 +34,11 @@ class DeleteCustomListConfirmationViewModel( val uiState = _error - .map { DeleteCustomListUiState(it) } + .map { DeleteCustomListUiState(name, it) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - DeleteCustomListUiState(null) + DeleteCustomListUiState(name, null) ) fun deleteCustomList() { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt index b1c57369889e..11e4071c9eef 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceListViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.DeviceListDestination import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -25,9 +27,12 @@ import net.mullvad.mullvadvpn.lib.shared.DeviceRepository class DeviceListViewModel( private val deviceRepository: DeviceRepository, - private val token: AccountNumber, + savedStateHandle: SavedStateHandle, private val dispatcher: CoroutineDispatcher = Dispatchers.Default, ) : ViewModel() { + private val accountNumber: AccountNumber = + DeviceListDestination.argsFrom(savedStateHandle).accountNumber + private val loadingDevices = MutableStateFlow>(emptySet()) private val deviceList = MutableStateFlow>(emptyList()) private val loading = MutableStateFlow(true) @@ -59,7 +64,9 @@ class DeviceListViewModel( viewModelScope.launch { error.value = null loading.value = true - deviceRepository.deviceList(token).fold({ error.value = it }, { deviceList.value = it }) + deviceRepository + .deviceList(accountNumber) + .fold({ error.value = it }, { deviceList.value = it }) loading.value = false } @@ -67,12 +74,12 @@ class DeviceListViewModel( viewModelScope.launch(dispatcher) { setLoadingState(deviceIdToRemove, true) deviceRepository - .removeDevice(token, deviceIdToRemove) + .removeDevice(accountNumber, deviceIdToRemove) .fold( { _uiSideEffect.send(DeviceListSideEffect.FailedToRemoveDevice) setLoadingState(deviceIdToRemove, false) - deviceRepository.deviceList(token).onRight { deviceList.value = it } + deviceRepository.deviceList(accountNumber).onRight { deviceList.value = it } }, { removeDeviceFromState(deviceIdToRemove) } ) @@ -82,6 +89,11 @@ class DeviceListViewModel( loadingDevices.update { if (isLoading) it + deviceId else it - deviceId } } + fun continueToLogin() = + viewModelScope.launch { + _uiSideEffect.send(DeviceListSideEffect.NavigateToLogin(accountNumber = accountNumber)) + } + private fun removeDeviceFromState(deviceId: DeviceId) { deviceList.update { devices -> devices.filter { item -> item.id != deviceId } } loadingDevices.update { it - deviceId } @@ -90,4 +102,6 @@ class DeviceListViewModel( sealed interface DeviceListSideEffect { data object FailedToRemoveDevice : DeviceListSideEffect + + data class NavigateToLogin(val accountNumber: AccountNumber) : DeviceListSideEffect } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt index 5df10ef0b1ed..14d154498d84 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DnsDialogViewModel.kt @@ -1,10 +1,12 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure +import com.ramcosta.composedestinations.generated.destinations.DnsDialogDestination import java.net.InetAddress import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -48,13 +50,13 @@ sealed class ValidationError { class DnsDialogViewModel( private val repository: SettingsRepository, private val inetAddressValidator: InetAddressValidator, - index: Int? = null, - initialValue: String?, + savedStateHandle: SavedStateHandle, private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : ViewModel() { + private val navArgs = DnsDialogDestination.argsFrom(savedStateHandle) - private val currentIndex = MutableStateFlow(index) - private val _ipAddressInput = MutableStateFlow(initialValue ?: EMPTY_STRING) + private val currentIndex = MutableStateFlow(navArgs.index) + private val _ipAddressInput = MutableStateFlow(navArgs.initialValue ?: EMPTY_STRING) val uiState: StateFlow = combine(_ipAddressInput, currentIndex, repository.settingsUpdates.filterNotNull()) { 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 043c50379229..d677b27f7ac8 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 @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.Either @@ -9,6 +10,7 @@ import arrow.core.getOrElse import arrow.core.nel import arrow.core.raise.either import arrow.core.raise.ensure +import com.ramcosta.composedestinations.generated.destinations.EditApiAccessMethodDestination import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel @@ -37,11 +39,13 @@ import net.mullvad.mullvadvpn.util.delayAtLeast import org.apache.commons.validator.routines.InetAddressValidator class EditApiAccessMethodViewModel( - private val apiAccessMethodId: ApiAccessMethodId?, private val apiAccessRepository: ApiAccessRepository, - private val inetAddressValidator: InetAddressValidator + private val inetAddressValidator: InetAddressValidator, + savedStateHandle: SavedStateHandle ) : ViewModel() { private var testingJob: Job? = null + private val apiAccessMethodId = + EditApiAccessMethodDestination.argsFrom(savedStateHandle).accessMethodId private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt index 2536f8edd75f..0b71a5053ead 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt @@ -1,29 +1,32 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.EditCustomListNameDestination import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.Renamed import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState -import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.RenameError class EditCustomListNameDialogViewModel( - private val customListId: CustomListId, - private val initialName: CustomListName, - private val customListActionUseCase: CustomListActionUseCase + private val customListActionUseCase: CustomListActionUseCase, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val navArgs = EditCustomListNameDestination.argsFrom(savedStateHandle) + private val inputName = MutableStateFlow(navArgs.initialName.value) + private val _uiSideEffect = Channel(1, BufferOverflow.DROP_OLDEST) val uiSideEffect = _uiSideEffect.receiveAsFlow() @@ -31,20 +34,19 @@ class EditCustomListNameDialogViewModel( private val _error = MutableStateFlow(null) val uiState = - _error - .map { EditCustomListNameUiState(name = initialName.value, error = it) } + combine(inputName, _error) { name, error -> EditCustomListNameUiState(name = name, error) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - EditCustomListNameUiState(name = initialName.value) + EditCustomListNameUiState(name = navArgs.initialName.value) ) fun updateCustomListName(name: String) { viewModelScope.launch { customListActionUseCase( CustomListAction.Rename( - id = customListId, - name = initialName, + id = navArgs.customListId, + name = navArgs.initialName, newName = CustomListName.fromString(name) ) ) @@ -55,7 +57,8 @@ class EditCustomListNameDialogViewModel( } } - fun clearError() { + fun onNameChanged(name: String) { + inputName.value = name viewModelScope.launch { _error.emit(null) } } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt index adfacceb4e9e..14295533d70c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.EditCustomListDestination import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -10,9 +12,12 @@ import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.repository.CustomListsRepository class EditCustomListViewModel( - private val customListId: CustomListId, - customListsRepository: CustomListsRepository + customListsRepository: CustomListsRepository, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val customListId: CustomListId = + EditCustomListDestination.argsFrom(savedStateHandle).customListId + val uiState = customListsRepository.customLists .map { customLists -> diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt index 9d1a17207c7b..fa29558f13e4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/MtuDialogViewModel.kt @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.MtuDialogDestination import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -17,12 +19,14 @@ import net.mullvad.mullvadvpn.repository.SettingsRepository class MtuDialogViewModel( private val repository: SettingsRepository, - private val initialMtu: Mtu?, + savedStateHandle: SavedStateHandle, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ViewModel() { + private val navArgs = MtuDialogDestination.argsFrom(savedStateHandle) - private val _mtuInput = MutableStateFlow(initialMtu?.value?.toString() ?: "") + private val _mtuInput = MutableStateFlow(navArgs.initialMtu?.value?.toString() ?: "") private val _isValidMtu = MutableStateFlow(true) + val uiState: StateFlow = combine(_mtuInput, _isValidMtu, ::createState) .stateIn( @@ -38,7 +42,7 @@ class MtuDialogViewModel( MtuDialogUiState( mtuInput = mtuInput, isValidInput = isValidMtuInput, - showResetToDefault = initialMtu != null + showResetToDefault = navArgs.initialMtu != null ) fun onInputChanged(value: String) { 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 be937e941641..e3bb3f783af9 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 @@ -1,7 +1,9 @@ package net.mullvad.mullvadvpn.viewmodel +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ramcosta.composedestinations.generated.destinations.SaveApiAccessMethodDestination import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -17,11 +19,14 @@ import net.mullvad.mullvadvpn.lib.model.NewAccessMethodSetting import net.mullvad.mullvadvpn.repository.ApiAccessRepository class SaveApiAccessMethodViewModel( - private val apiAccessMethodId: ApiAccessMethodId?, - private val apiAccessMethodName: ApiAccessMethodName, - private val customProxy: ApiAccessMethod.CustomProxy, - private val apiAccessRepository: ApiAccessRepository + private val apiAccessRepository: ApiAccessRepository, + savedStateHandle: SavedStateHandle ) : ViewModel() { + private val navArgs = SaveApiAccessMethodDestination.argsFrom(savedStateHandle) + private val apiAccessMethodId: ApiAccessMethodId? = navArgs.id + private val apiAccessMethodName: ApiAccessMethodName = navArgs.name + private val customProxy: ApiAccessMethod.CustomProxy = navArgs.customProxy + private val _uiSideEffect = Channel() val uiSideEffect = _uiSideEffect.receiveAsFlow() private val _uiState = MutableStateFlow(SaveApiAccessMethodUiState()) diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModelTest.kt index 63ce73c1a3ae..bd304573d3da 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ApiAccessMethodDetailsViewModelTest.kt @@ -4,6 +4,7 @@ import app.cash.turbine.test import arrow.core.Either import arrow.core.left import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -14,6 +15,7 @@ import kotlin.test.assertIs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import kotlinx.coroutines.time.delay +import net.mullvad.mullvadvpn.compose.screen.ApiAccessMethodDetailsNavArgs import net.mullvad.mullvadvpn.compose.state.ApiAccessMethodDetailsUiState import net.mullvad.mullvadvpn.data.UUID import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule @@ -49,8 +51,10 @@ class ApiAccessMethodDetailsViewModelTest { apiAccessMethodDetailsViewModel = ApiAccessMethodDetailsViewModel( - apiAccessMethodId = apiAccessMethodId, - apiAccessRepository = mockApiAccessRepository + apiAccessRepository = mockApiAccessRepository, + savedStateHandle = + ApiAccessMethodDetailsNavArgs(accessMethodId = apiAccessMethodId) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt index efdf08447dd2..2ab6e6267bf3 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModelTest.kt @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test import arrow.core.left import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -10,6 +11,7 @@ import kotlin.test.assertIs import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.communication.Created import net.mullvad.mullvadvpn.compose.communication.CustomListAction +import net.mullvad.mullvadvpn.compose.dialog.CreateCustomListNavArgs import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists import net.mullvad.mullvadvpn.lib.model.CustomListId @@ -113,7 +115,11 @@ class CreateCustomListDialogViewModelTest { private fun createViewModelWithLocationCode(locationCode: GeoLocationId) = CreateCustomListDialogViewModel( - locationCode = locationCode, - customListActionUseCase = mockCustomListActionUseCase + customListActionUseCase = mockCustomListActionUseCase, + savedStateHandle = + CreateCustomListNavArgs( + locationCode = locationCode, + ) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt index 20130018d8bd..4879031ce72e 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.LocationsChanged +import net.mullvad.mullvadvpn.compose.screen.CustomListLocationsNavArgs import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.CustomList @@ -221,11 +223,15 @@ class CustomListLocationsViewModelTest { newList: Boolean ): CustomListLocationsViewModel { return CustomListLocationsViewModel( - customListId = customListId, - newList = newList, relayListRepository = mockRelayListRepository, customListRelayItemsUseCase = mockCustomListRelayItemsUseCase, - customListActionUseCase = mockCustomListUseCase + customListActionUseCase = mockCustomListUseCase, + savedStateHandle = + CustomListLocationsNavArgs( + customListId = customListId, + newList = newList, + ) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModelTest.kt index 18f6a64647a5..eea3c01b2d3f 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteApiAccessMethodConfirmationViewModelTest.kt @@ -3,9 +3,11 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test import arrow.core.left import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.dialog.DeleteApiAccessMethodNavArgs import net.mullvad.mullvadvpn.data.UUID import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodId @@ -29,8 +31,10 @@ class DeleteApiAccessMethodConfirmationViewModelTest { deleteApiAccessMethodConfirmationViewModel = DeleteApiAccessMethodConfirmationViewModel( - apiAccessMethodId = apiAccessMethodId, - apiAccessRepository = mockApiAccessRepository + apiAccessRepository = mockApiAccessRepository, + savedStateHandle = + DeleteApiAccessMethodNavArgs(apiAccessMethodId = apiAccessMethodId) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt index 66307a499d69..2656d1a4ff38 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModelTest.kt @@ -2,14 +2,17 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.mockk import kotlin.test.assertIs import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.Deleted +import net.mullvad.mullvadvpn.compose.dialog.DeleteCustomListNavArgs import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.CustomListId +import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -38,7 +41,12 @@ class DeleteCustomListConfirmationViewModelTest { private fun createViewModel() = DeleteCustomListConfirmationViewModel( - customListId = CustomListId("1"), - customListActionUseCase = mockCustomListActionUseCase + customListActionUseCase = mockCustomListActionUseCase, + savedStateHandle = + DeleteCustomListNavArgs( + customListId = CustomListId("1"), + name = CustomListName.fromString("asdf") + ) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt index 130ca21eceeb..7b4dcc0b8321 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModelTest.kt @@ -3,12 +3,14 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test import arrow.core.left import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.mockk import kotlin.test.assertIs import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.Renamed +import net.mullvad.mullvadvpn.compose.dialog.EditCustomListNameNavArgs import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName @@ -16,7 +18,6 @@ import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.usecase.customlists.RenameError import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -77,15 +78,17 @@ class EditCustomListNameDialogViewModelTest { awaitItem() // Default state viewModel.updateCustomListName(customListName) assertEquals(expectedError, awaitItem().error) // Showing error - viewModel.clearError() - assertNull(awaitItem().error) } } private fun createViewModel(customListId: CustomListId, initialName: String) = EditCustomListNameDialogViewModel( - customListId = customListId, - initialName = CustomListName.fromString(initialName), - customListActionUseCase = mockCustomListActionUseCase + customListActionUseCase = mockCustomListActionUseCase, + savedStateHandle = + EditCustomListNameNavArgs( + customListId = customListId, + initialName = CustomListName.fromString(initialName), + ) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt index c3f233846a9a..16d1488bc41d 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListViewModelTest.kt @@ -1,11 +1,13 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.every import io.mockk.mockk import kotlin.test.assertIs import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.dialog.EditCustomListNameNavArgs import net.mullvad.mullvadvpn.compose.state.EditCustomListState import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule import net.mullvad.mullvadvpn.lib.model.CustomList @@ -24,14 +26,10 @@ class EditCustomListViewModelTest { fun `given a custom list id that does not exists should return not found ui state`() = runTest { // Arrange val customListId = CustomListId("2") - val customList = - CustomList( - id = CustomListId("1"), - name = CustomListName.fromString("test"), - locations = emptyList() - ) + val name = CustomListName.fromString("test") + val customList = CustomList(id = CustomListId("1"), name = name, locations = emptyList()) every { mockCustomListsRepository.customLists } returns MutableStateFlow(listOf(customList)) - val viewModel = createViewModel(customListId) + val viewModel = createViewModel(customListId, name) // Act, Assert viewModel.uiState.test { @@ -44,14 +42,10 @@ class EditCustomListViewModelTest { fun `given a custom list id that exists should return content ui state`() = runTest { // Arrange val customListId = CustomListId("1") - val customList = - CustomList( - id = customListId, - name = CustomListName.fromString("test"), - locations = emptyList() - ) + val name = CustomListName.fromString("test") + val customList = CustomList(id = customListId, name = name, locations = emptyList()) every { mockCustomListsRepository.customLists } returns MutableStateFlow(listOf(customList)) - val viewModel = createViewModel(customListId) + val viewModel = createViewModel(customListId, name) // Act, Assert viewModel.uiState.test { @@ -63,9 +57,11 @@ class EditCustomListViewModelTest { } } - private fun createViewModel(customListId: CustomListId) = + private fun createViewModel(customListId: CustomListId, initialName: CustomListName) = EditCustomListViewModel( - customListId = customListId, - customListsRepository = mockCustomListsRepository + customListsRepository = mockCustomListsRepository, + savedStateHandle = + EditCustomListNameNavArgs(customListId = customListId, initialName = initialName) + .toSavedStateHandle() ) } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModelTest.kt index 0828b3ed0841..3455bba423f5 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SaveApiAccessMethodViewModelTest.kt @@ -3,10 +3,12 @@ package net.mullvad.mullvadvpn.viewmodel import app.cash.turbine.test import arrow.core.left import arrow.core.right +import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle import io.mockk.coEvery import io.mockk.coVerify import io.mockk.mockk import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.compose.dialog.SaveApiAccessMethodNavArgs import net.mullvad.mullvadvpn.compose.state.SaveApiAccessMethodUiState import net.mullvad.mullvadvpn.compose.state.TestApiAccessMethodState import net.mullvad.mullvadvpn.data.UUID @@ -212,10 +214,14 @@ class SaveApiAccessMethodViewModelTest { ) { saveApiAccessMethodViewModel = SaveApiAccessMethodViewModel( - apiAccessMethodId = apiAccessMethodId, - apiAccessMethodName = apiAccessMethodName, - customProxy = customProxy, - apiAccessRepository = mockApiAccessRepository + apiAccessRepository = mockApiAccessRepository, + savedStateHandle = + SaveApiAccessMethodNavArgs( + id = apiAccessMethodId, + name = apiAccessMethodName, + customProxy = customProxy, + ) + .toSavedStateHandle() ) } } diff --git a/android/config/baseline.xml b/android/config/baseline.xml index 9f7bc424b7c1..36bd121a1266 100644 --- a/android/config/baseline.xml +++ b/android/config/baseline.xml @@ -6,9 +6,9 @@ EmptyFunctionBlock:AccountTestRule.kt$AccountTestRule${} EmptyKtFile:build.gradle.kts$.build.gradle.kts LargeClass:ConnectScreenTest.kt$ConnectScreenTest - LongMethod:ApiAccessMethodDetailsScreen.kt$@Destination<RootGraph>(style = SlideInFromRightTransition::class) @Composable fun ApiAccessMethodDetails( navigator: DestinationsNavigator, accessMethodId: ApiAccessMethodId, confirmDeleteListResultRecipient: ResultRecipient<DeleteApiAccessMethodConfirmationDestination, Boolean> ) + LongMethod:ApiAccessMethodDetailsScreen.kt$@Destination<RootGraph>( style = SlideInFromRightTransition::class, navArgs = ApiAccessMethodDetailsNavArgs::class ) @Composable fun ApiAccessMethodDetails( navigator: DestinationsNavigator, confirmDeleteListResultRecipient: ResultRecipient<DeleteApiAccessMethodConfirmationDestination, Boolean> ) LongMethod:ConnectionButton.kt$@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ConnectionButton( text: String, mainClick: () -> Unit, reconnectClick: () -> Unit, isReconnectButtonEnabled: Boolean, containerColor: Color, contentColor: Color, modifier: Modifier = Modifier, reconnectButtonTestTag: String = "" ) - LongMethod:EditApiAccessMethodScreen.kt$@Destination<RootGraph>(style = SlideInFromRightTransition::class) @Composable fun EditApiAccessMethod( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator<Boolean>, saveApiAccessMethodResultRecipient: ResultRecipient<SaveApiAccessMethodDestination, Boolean>, discardChangesResultRecipient: ResultRecipient<DiscardChangesDialogDestination, Boolean>, accessMethodId: ApiAccessMethodId? ) + LongMethod:EditApiAccessMethodScreen.kt$@Destination<RootGraph>( style = SlideInFromRightTransition::class, navArgs = EditApiAccessMethodNavArgs::class ) @Composable fun EditApiAccessMethod( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator<Boolean>, saveApiAccessMethodResultRecipient: ResultRecipient<SaveApiAccessMethodDestination, Boolean>, discardChangesResultRecipient: ResultRecipient<DiscardChangesDialogDestination, Boolean>, ) LongMethod:NotificationBanner.kt$@Composable private fun Notification(notificationBannerData: NotificationData) MagicNumber:Chevron.kt$100 MagicNumber:Chevron.kt$270f diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt index da59481269ad..c560fab49c65 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt @@ -17,7 +17,8 @@ value class CustomListId(val value: String) : RelayItemId, Parcelable { } @optics -sealed interface GeoLocationId : RelayItemId { +@Parcelize +sealed interface GeoLocationId : RelayItemId, Parcelable { @optics @Parcelize data class Country(val countryCode: String) : GeoLocationId {