From f96ecd8151346afdeb4bf0e71784aa4a6ac90106 Mon Sep 17 00:00:00 2001 From: Iulia Stana Date: Wed, 12 Apr 2023 09:24:43 +0200 Subject: [PATCH 1/6] Edured-78: Add implementation for remove service Remove unrequired Companion object wrapper for Provider. Make sure the data and activity screen can show loading state & error messages. Refactored screen to use the existing top app bar. Navigate to remove service warning screen from Data & Activity screen. Grouped navigation graph destinations. --- .../main/kotlin/nl/eduid/di/api/EduIdApi.kt | 3 + .../kotlin/nl/eduid/di/model/EduIdModels.kt | 17 +- .../main/kotlin/nl/eduid/graphs/MainGraph.kt | 79 +++++--- app/src/main/kotlin/nl/eduid/graphs/Routes.kt | 13 ++ .../dataactivity/DataAndActivityData.kt | 16 +- .../dataactivity/DataAndActivityScreen.kt | 146 +++++++-------- .../dataactivity/DataAndActivityViewModel.kt | 50 ++++- .../dataactivity/DeleteServiceScreen.kt | 173 ++++++++++++++++++ .../nl/eduid/screens/dataactivity/UiState.kt | 10 + .../DeleteAccountSecondConfirmScreen.kt | 2 +- .../personalinfo/PersonalInfoRepository.kt | 18 ++ app/src/main/kotlin/nl/eduid/ui/InfoTab.kt | 6 +- app/src/main/res/values/strings.xml | 7 +- 13 files changed, 407 insertions(+), 133 deletions(-) create mode 100644 app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt create mode 100644 app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt diff --git a/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt b/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt index abaa97ee..a0dba43d 100644 --- a/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt +++ b/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt @@ -38,4 +38,7 @@ interface EduIdApi { @PUT("/mobile/api/sp/institution") suspend fun removeConnection(@Body account: LinkedAccount): Response + + @PUT("/mobile/api/sp/service") + suspend fun removeService(@Body serviceId: DeleteServiceRequest): Response } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt b/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt index ad701331..6a2cc45e 100644 --- a/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt +++ b/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt @@ -42,8 +42,7 @@ const val EMAIL_DOMAIN_FORBIDDEN = 412 data class EnrollResponse( val url: String, val enrollmentKey: String, - @Json(name = "qrcode") - val qrCode: String, + @Json(name = "qrcode") val qrCode: String, ) : Parcelable @Parcelize @@ -66,7 +65,7 @@ data class UserDetails( val eduIdPerServiceProvider: Map, val loginOptions: List, - val registration: Registration? + val registration: Registration?, ) : Parcelable { fun isRecoveryRequired(): Boolean = registration?.status != "FINALIZED" @@ -80,7 +79,7 @@ data class EduIdPerServiceProvider( val serviceName: String, val serviceNameNl: String, val serviceLogoUrl: String, - val createdAt: Long + val createdAt: Long, ) : Parcelable @Parcelize @@ -94,7 +93,7 @@ data class LinkedAccount( val familyName: String, val eduPersonAffiliations: List, val createdAt: Long, - val expiresAt: Long + val expiresAt: Long, ) : Parcelable @Parcelize @@ -113,10 +112,16 @@ data class InstitutionNameResponse( val displayNameEn: String, val displayNameNl: String, -) : Parcelable + ) : Parcelable @Parcelize @JsonClass(generateAdapter = true) data class EmailChangeRequest( val email: String, +) : Parcelable + +@Parcelize +@JsonClass(generateAdapter = true) +data class DeleteServiceRequest( + val serviceId: String, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt b/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt index fd0aa6f2..4fd739b6 100644 --- a/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt +++ b/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt @@ -19,6 +19,7 @@ import nl.eduid.screens.biometric.EnableBiometricViewModel import nl.eduid.screens.created.RequestEduIdCreatedScreen import nl.eduid.screens.dataactivity.DataAndActivityScreen import nl.eduid.screens.dataactivity.DataAndActivityViewModel +import nl.eduid.screens.dataactivity.DeleteServiceScreen import nl.eduid.screens.deeplinks.DeepLinkScreen import nl.eduid.screens.deeplinks.DeepLinkViewModel import nl.eduid.screens.deleteaccountfirstconfirm.DeleteAccountFirstConfirmScreen @@ -379,15 +380,63 @@ fun MainGraph( }, ) { navController.popBackStack() } } + //region Delete Account + composable( + route = ManageAccountRoute.routeWithArgs, arguments = ManageAccountRoute.arguments + ) { entry -> + val viewModel = hiltViewModel(entry) + ManageAccountScreen( + viewModel = viewModel, + goBack = { navController.popBackStack() }, + onDeleteAccountPressed = { navController.navigate(Graph.DELETE_ACCOUNT_FIRST_CONFIRM) }, + dateString = ManageAccountRoute.decodeDateFromEntry(entry), + ) + } + + composable(Graph.DELETE_ACCOUNT_FIRST_CONFIRM) { + DeleteAccountFirstConfirmScreen( + goBack = { navController.popBackStack() }, + onDeleteAccountPressed = { navController.navigate(Graph.DELETE_ACCOUNT_SECOND_CONFIRM) }, + ) + } + + composable(Graph.DELETE_ACCOUNT_SECOND_CONFIRM) { + val viewModel = hiltViewModel(it) + DeleteAccountSecondConfirmScreen( + viewModel = viewModel, + goBack = { navController.popBackStack() }, + ) + }//endregion //endregion + //region Data and activity composable(Graph.DATA_AND_ACTIVITY) { val viewModel = hiltViewModel(it) DataAndActivityScreen( viewModel = viewModel, goBack = { navController.popBackStack() }, - onDeleteLoginClicked = {}, + goToConfirmDeleteService = { + navController.navigate( + ConfirmDeleteService.routeForIndex( + it + ) + ) + }, ) } + composable( + route = ConfirmDeleteService.routeWithArgs, arguments = ConfirmDeleteService.arguments + ) { entry -> + val viewModel = hiltViewModel(entry) + val index = entry.arguments?.getInt(ConfirmDeleteService.serviceIndexArg, 0) ?: 0 + DeleteServiceScreen( + viewModel = viewModel, + goBack = { navController.popBackStack() }, + index = index, + ) + } + + //endregion + //region Security composable(Graph.SECURITY) { val viewModel = hiltViewModel(it) SecurityScreen( @@ -421,33 +470,7 @@ fun MainGraph( onSaveNewEmailRequested = { email -> navController.goToEmailSent(email) }, ) } - - composable( - route = ManageAccountRoute.routeWithArgs, arguments = ManageAccountRoute.arguments - ) { entry -> - val viewModel = hiltViewModel(entry) - ManageAccountScreen( - viewModel = viewModel, - goBack = { navController.popBackStack() }, - onDeleteAccountPressed = { navController.navigate(Graph.DELETE_ACCOUNT_FIRST_CONFIRM) }, - dateString = ManageAccountRoute.decodeDateFromEntry(entry), - ) - } - - composable(Graph.DELETE_ACCOUNT_FIRST_CONFIRM) { - DeleteAccountFirstConfirmScreen( - goBack = { navController.popBackStack() }, - onDeleteAccountPressed = { navController.navigate(Graph.DELETE_ACCOUNT_SECOND_CONFIRM) }, - ) - } - - composable(Graph.DELETE_ACCOUNT_SECOND_CONFIRM) { - val viewModel = hiltViewModel(it) - DeleteAccountSecondConfirmScreen( - viewModel = viewModel, - goBack = { navController.popBackStack() }, - ) - } + //endregion } private fun NavController.goToEmailSent(email: String) = navigate( diff --git a/app/src/main/kotlin/nl/eduid/graphs/Routes.kt b/app/src/main/kotlin/nl/eduid/graphs/Routes.kt index b8204577..149ea145 100644 --- a/app/src/main/kotlin/nl/eduid/graphs/Routes.kt +++ b/app/src/main/kotlin/nl/eduid/graphs/Routes.kt @@ -66,6 +66,19 @@ object RequestEduIdLinkSent { } } +object ConfirmDeleteService { + private const val route = "confirm_delete_service" + const val serviceIndexArg = "serviceIndexArg" + val routeWithArgs = "$route/{$serviceIndexArg}" + val arguments = listOf(navArgument(serviceIndexArg) { + type = NavType.IntType + nullable = false + defaultValue = 0 + }) + + fun routeForIndex(index: Int) = "$route/$index" +} + sealed class PhoneNumberRecovery(val route: String) { object RequestCode : PhoneNumberRecovery("phone_number_recover") object ConfirmCode : PhoneNumberRecovery("phone_number_confirm_code") { diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt index e95856af..14840db6 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt @@ -3,13 +3,11 @@ package nl.eduid.screens.dataactivity data class DataAndActivityData( val providerList: List? = null, ) { - companion object { - data class Provider( - val providerName: String, - val createdStamp: Long, - val firstLoginStamp: Long, - val uniqueId: String, - val providerLogoUrl: String, - ) - } + data class Provider( + val providerName: String, + val createdStamp: Long, + val firstLoginStamp: Long, + val uniqueId: String, + val providerLogoUrl: String, + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt index 4130405b..3fbe47fa 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt @@ -8,14 +8,15 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import nl.eduid.ErrorData import nl.eduid.R +import nl.eduid.ui.AlertDialogWithSingleButton +import nl.eduid.ui.EduIdTopAppBar import nl.eduid.ui.InfoTab -import nl.eduid.ui.getDateString import nl.eduid.ui.getDateTimeString import nl.eduid.ui.theme.ButtonGreen import nl.eduid.ui.theme.EduidAppAndroidTheme @@ -23,96 +24,81 @@ import nl.eduid.ui.theme.EduidAppAndroidTheme @Composable fun DataAndActivityScreen( viewModel: DataAndActivityViewModel, - onDeleteLoginClicked: () -> Unit, + goToConfirmDeleteService: (Int) -> Unit, goBack: () -> Unit, +) = EduIdTopAppBar( + onBackClicked = goBack, ) { - val dataAndActivity by viewModel.dataAndActivity.observeAsState(DataAndActivityData()) + val uiState by viewModel.uiState.observeAsState(UiState()) DataAndActivityScreenContent( - onDeleteLoginClicked = { }, - goBack = goBack, - dataAndActivity = dataAndActivity, + dataAndActivity = uiState.data, + isLoading = uiState.isLoading, + errorData = uiState.errorData, + dismissError = viewModel::clearErrorData, + goToConfirmDeleteService = goToConfirmDeleteService, ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun DataAndActivityScreenContent( - onDeleteLoginClicked: () -> Unit, - goBack: () -> Unit, dataAndActivity: DataAndActivityData, + isLoading: Boolean = false, + errorData: ErrorData? = null, + dismissError: () -> Unit = {}, + goToConfirmDeleteService: (Int) -> Unit = {}, +) = Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier + .verticalScroll(rememberScrollState()) ) { - Scaffold( - topBar = { - CenterAlignedTopAppBar( - modifier = Modifier.padding(top = 42.dp, start = 26.dp, end = 26.dp), - navigationIcon = { - Image( - painter = painterResource(R.drawable.back_button_icon), - contentDescription = "", - modifier = Modifier - .size(width = 46.dp, height = 46.dp) - .clickable { - goBack.invoke() - }, - alignment = Alignment.Center - ) - }, - title = { - Image( - painter = painterResource(R.drawable.ic_top_logo), - contentDescription = "", - modifier = Modifier.size(width = 122.dp, height = 46.dp), - alignment = Alignment.Center - ) - }, - ) - }, - ) { paddingValues -> - Column( - verticalArrangement = Arrangement.Bottom, + if (errorData != null) { + AlertDialogWithSingleButton( + title = errorData.title, + explanation = errorData.message, + buttonLabel = stringResource(R.string.button_ok), + onDismiss = dismissError + ) + } + Spacer(Modifier.height(36.dp)) + Text( + style = MaterialTheme.typography.titleLarge.copy( + textAlign = TextAlign.Start, + color = ButtonGreen + ), + text = stringResource(R.string.data_info_title), + modifier = Modifier + .fillMaxWidth() + ) + Spacer(Modifier.height(12.dp)) + Text( + style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Start), + text = stringResource(R.string.data_info_subtitle), + modifier = Modifier + .fillMaxWidth() + ) + Spacer(Modifier.height(12.dp)) + if (isLoading) { + Spacer(Modifier.height(24.dp)) + CircularProgressIndicator( modifier = Modifier - .padding(paddingValues) - .padding(start = 26.dp, end = 26.dp) - .verticalScroll(rememberScrollState()) - ) { - Spacer(Modifier.height(36.dp)) - Text( - style = MaterialTheme.typography.titleLarge.copy( - textAlign = TextAlign.Start, - color = ButtonGreen + .height(80.dp) + .width(80.dp) + .align(alignment = Alignment.CenterHorizontally) + ) + } else { + dataAndActivity.providerList?.forEachIndexed { index, provider -> + InfoTab( + startIconLargeUrl = provider.providerLogoUrl, + title = provider.providerName, + subtitle = stringResource( + R.string.data_info_on_date, + provider.firstLoginStamp.getDateTimeString() ), - text = stringResource(R.string.data_info_title), - modifier = Modifier - .fillMaxWidth() - ) - Spacer(Modifier.height(12.dp)) - Text( - style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Start), - text = stringResource(R.string.data_info_subtitle), - modifier = Modifier - .fillMaxWidth() + onClick = { }, + onDeleteButtonClicked = { goToConfirmDeleteService(index) }, + endIcon = R.drawable.chevron_down, + serviceProviderInfo = provider, ) - Spacer(Modifier.height(12.dp)) - if (dataAndActivity.providerList == null) { - Spacer(Modifier.height(24.dp)) - CircularProgressIndicator( - modifier = Modifier - .height(80.dp) - .width(80.dp) - .align(alignment = Alignment.CenterHorizontally) - ) - } else { - dataAndActivity.providerList.forEach { provider -> - InfoTab( - startIconLargeUrl = provider.providerLogoUrl, - title = provider.providerName, - subtitle = "on ${provider.firstLoginStamp.getDateTimeString()}", - onClick = { }, - endIcon = R.drawable.chevron_down, - serviceProviderInfo = provider, - ) - } - } } } } @@ -122,8 +108,6 @@ fun DataAndActivityScreenContent( @Composable private fun PreviewDataAndActivityScreenContent() = EduidAppAndroidTheme { DataAndActivityScreenContent( - onDeleteLoginClicked = { }, - goBack = { }, dataAndActivity = DataAndActivityData(), ) } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt index d77dd341..b4654e08 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import nl.eduid.ErrorData import nl.eduid.di.model.UserDetails import nl.eduid.screens.personalinfo.PersonalInfoRepository import javax.inject.Inject @@ -12,27 +13,68 @@ import javax.inject.Inject @HiltViewModel class DataAndActivityViewModel @Inject constructor(private val repository: PersonalInfoRepository) : ViewModel() { - val dataAndActivity = MutableLiveData() + val uiState = MutableLiveData() init { viewModelScope.launch { + uiState.postValue(UiState(isLoading = true, errorData = null)) val userDetails = repository.getUserDetails() if (userDetails != null) { val uiData = convertToUiData(userDetails) - dataAndActivity.postValue(uiData) + uiState.postValue(UiState(isLoading = false, errorData = null, data = uiData)) + } else { + uiState.postValue( + UiState( + isLoading = false, + errorData = ErrorData( + "Failed to load data", + "Could not load activity history" + ) + ) + ) } } } + fun clearErrorData() { + uiState.value = uiState.value?.copy(errorData = null) + } + + fun removeService(service: String?) = viewModelScope.launch { + val serviceId = service ?: return@launch + uiState.postValue(UiState(isLoading = true, errorData = null)) + val userDetails = repository.removeService(serviceId) + if (userDetails != null) { + val uiData = convertToUiData(userDetails) + uiState.postValue( + UiState( + isLoading = false, + errorData = null, + data = uiData, + isComplete = Unit + ) + ) + } else { + uiState.postValue( + UiState( + isLoading = false, + errorData = ErrorData( + "Failed to load data", + "Could not load activity history" + ), + ) + ) + } + } + private fun convertToUiData(userDetails: UserDetails): DataAndActivityData { val providers = userDetails.eduIdPerServiceProvider.values.map { - DataAndActivityData.Companion.Provider( + DataAndActivityData.Provider( providerName = it.serviceName, createdStamp = it.createdAt, firstLoginStamp = it.createdAt, uniqueId = it.value, providerLogoUrl = it.serviceLogoUrl, - ) } diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt new file mode 100644 index 00000000..487a1917 --- /dev/null +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt @@ -0,0 +1,173 @@ +package nl.eduid.screens.dataactivity + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import nl.eduid.R +import nl.eduid.ui.EduIdTopAppBar +import nl.eduid.ui.PrimaryButton +import nl.eduid.ui.SecondaryButton +import nl.eduid.ui.theme.AlertRedBackground +import nl.eduid.ui.theme.ButtonRed +import nl.eduid.ui.theme.EduidAppAndroidTheme +import nl.eduid.ui.theme.TextGreen + +@Composable +fun DeleteServiceScreen( + viewModel: DataAndActivityViewModel, + index: Int, + goBack: () -> Unit = {}, +) = EduIdTopAppBar( + onBackClicked = goBack, +) { + val uiState by viewModel.uiState.observeAsState(UiState()) + val provider by remember(index) { + derivedStateOf { + uiState.data.providerList?.get(index) + } + } + DeleteServiceContent( + providerName = provider?.providerName.orEmpty(), + inProgress = uiState.isLoading, + removeService = { viewModel.removeService(provider?.uniqueId) }, + goBack = goBack + ) +} + +@Composable +private fun DeleteServiceContent( + providerName: String, + isComplete: Unit? = null, + inProgress: Boolean = false, + removeService: () -> Unit = {}, + goBack: () -> Unit = {}, +) { + var isProcessing by rememberSaveable { mutableStateOf(false) } + val owner = LocalLifecycleOwner.current + if (isProcessing && isComplete != null) { + val currentGoBack by rememberUpdatedState(goBack) + LaunchedEffect(owner) { + isProcessing = true + currentGoBack() + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + Text( + text = stringResource(R.string.delete_service_confirm_title), + style = MaterialTheme.typography.titleLarge.copy( + color = TextGreen, textAlign = TextAlign.Start + ), + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(18.dp)) + if (inProgress) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(16.dp)) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .background(color = AlertRedBackground) + ) { + Image( + painter = painterResource(R.drawable.warning_icon_red), + contentDescription = "", + modifier = Modifier.padding(12.dp) + ) + Text( + style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), + text = stringResource(R.string.delete_no_undo_warning), + modifier = Modifier.fillMaxWidth() + ) + } + + Spacer(Modifier.height(18.dp)) + Text( + text = stringResource( + R.string.delete_service_confirm_explanation, providerName + ), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Justify, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(36.dp)) + } + Row( + horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth() + ) { + SecondaryButton( + modifier = Modifier.widthIn(min = 140.dp), + text = stringResource(R.string.button_cancel), + onClick = goBack, + ) + PrimaryButton( + modifier = Modifier.widthIn(min = 140.dp), + text = stringResource(R.string.button_confirm), + onClick = { + isProcessing = true + removeService() + }, + buttonBackgroundColor = ButtonRed, + buttonTextColor = Color.White, + ) + } + Spacer(Modifier.height(24.dp)) + } +} + +@Preview() +@Composable +private fun PreviewDeleteServiceScreen() { + EduidAppAndroidTheme { + DeleteServiceContent( + providerName = "OpenConext Profile", + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt new file mode 100644 index 00000000..57204dde --- /dev/null +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt @@ -0,0 +1,10 @@ +package nl.eduid.screens.dataactivity + +import nl.eduid.ErrorData + +data class UiState( + val data: DataAndActivityData = DataAndActivityData(), + val isLoading: Boolean = false, + val errorData: ErrorData? = null, + val isComplete: Unit? = null, +) \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/deleteaccountsecondconfirm/DeleteAccountSecondConfirmScreen.kt b/app/src/main/kotlin/nl/eduid/screens/deleteaccountsecondconfirm/DeleteAccountSecondConfirmScreen.kt index 4264054c..1399b5d8 100644 --- a/app/src/main/kotlin/nl/eduid/screens/deleteaccountsecondconfirm/DeleteAccountSecondConfirmScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/deleteaccountsecondconfirm/DeleteAccountSecondConfirmScreen.kt @@ -122,7 +122,7 @@ private fun DeleteAccountSecondConfirmScreenContent( ) Text( style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold), - text = stringResource(R.string.delete_account_two_subtitle), + text = stringResource(R.string.delete_no_undo_warning), modifier = Modifier .constrainAs(text) { start.linkTo(image.end) diff --git a/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt b/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt index 973bb501..a77903f2 100644 --- a/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt +++ b/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt @@ -1,6 +1,7 @@ package nl.eduid.screens.personalinfo import nl.eduid.di.api.EduIdApi +import nl.eduid.di.model.DeleteServiceRequest import nl.eduid.di.model.LinkedAccount import nl.eduid.di.model.UserDetails import timber.log.Timber @@ -24,6 +25,23 @@ class PersonalInfoRepository(private val eduIdApi: EduIdApi) { null } + suspend fun removeService(serviceId: String): UserDetails? = try { + val response = eduIdApi.removeService(DeleteServiceRequest(serviceId = serviceId)) + if (response.isSuccessful) { + response.body() + } else { + Timber.w( + "Failed to remove connection for [${response.code()}/${response.message()}]${ + response.errorBody()?.string() + }" + ) + null + } + } catch (e: Exception) { + Timber.e(e, "Failed to remove service with id $serviceId") + null + } + suspend fun removeConnection(linkedAccount: LinkedAccount): UserDetails? = try { val response = eduIdApi.removeConnection(linkedAccount) if (response.isSuccessful) { diff --git a/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt b/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt index 320261eb..f5e8717a 100644 --- a/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt +++ b/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt @@ -52,7 +52,7 @@ fun InfoTab( onClick: () -> Unit, enabled: Boolean = true, institutionInfo: PersonalInfo.Companion.InstitutionAccount? = null, - serviceProviderInfo: DataAndActivityData.Companion.Provider? = null, + serviceProviderInfo: DataAndActivityData.Provider? = null, onDeleteButtonClicked: () -> Unit = { }, startIconLargeUrl: String = "", @DrawableRes endIcon: Int = 0, @@ -293,7 +293,7 @@ private fun InstitutionInfoBlock( @Composable private fun serviceProviderBlock( - serviceProviderInfo: DataAndActivityData.Companion.Provider, + serviceProviderInfo: DataAndActivityData.Provider, onDeleteButtonClicked: () -> Unit ): @Composable() (ColumnScope.() -> Unit) = { @@ -367,7 +367,7 @@ private fun serviceProviderBlock( .fillMaxWidth(), ) { Text( - text = "Delete login details *", + text = stringResource(R.string.infotab_delete_login_details), style = MaterialTheme.typography.bodyLarge.copy( color = ButtonRed, fontWeight = FontWeight.SemiBold ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67fd2bb4..baeb46cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,6 +132,7 @@ Data & Activity Each service you accessed through eduID receives certain personal data (attributes) from your eduID account. E.g. your name & email address or a pseudonym which the service can use to uniquely identify you. Each service you accessed through eduID receives certain personal data (attributes) from your eduID account. E.g. your name & email address or a pseudonym which the service can use to uniquely identify you. + on %s Security settings We provide different methods to sign in to your eduID account. @@ -190,7 +191,7 @@ You can delete your eduID whenever you want.\n\nWhen you re-register for a new eduID with that same email address, you will receive a new eduID number. Some services use this unique number to identify you, so for those services you will be treated as a new user. Please note that deleting your eduID account does not mean all services you accessed with that eduID account will also have your data removed. Delete your account for all eternity? - There is no way to undo this action + There is no way to undo this action If you wish to proceed, please type in your full name for confirmation. Remove connection Login Details @@ -202,5 +203,9 @@ At %s Role & institution Manage your account + Delete login details * + Delete service + Are you sure you want to delete your unique pseudonymised eduID for %s and revoke access to your linked accounts?This service might not recognize you the next time you login and all your personal data within this service might be lost. + Confirm From b2a71dc6f38962d0ff6a966dfd65ccd281bba9f7 Mon Sep 17 00:00:00 2001 From: Iulia Stana Date: Wed, 12 Apr 2023 10:02:23 +0200 Subject: [PATCH 2/6] Edured-78: Change API call to the latest implementation --- app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt | 2 +- .../nl/eduid/screens/dataactivity/DataAndActivityData.kt | 1 + .../nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt | 1 + .../nl/eduid/screens/dataactivity/DeleteServiceScreen.kt | 4 ++-- .../nl/eduid/screens/personalinfo/PersonalInfoRepository.kt | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt b/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt index 6a2cc45e..a8d1c622 100644 --- a/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt +++ b/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt @@ -123,5 +123,5 @@ data class EmailChangeRequest( @Parcelize @JsonClass(generateAdapter = true) data class DeleteServiceRequest( - val serviceId: String, + val serviceProviderEntityId: String, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt index 14840db6..b19ed778 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt @@ -8,6 +8,7 @@ data class DataAndActivityData( val createdStamp: Long, val firstLoginStamp: Long, val uniqueId: String, + val serviceProviderEntityId: String, val providerLogoUrl: String, ) } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt index b4654e08..8b38513b 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt @@ -74,6 +74,7 @@ class DataAndActivityViewModel @Inject constructor(private val repository: Perso createdStamp = it.createdAt, firstLoginStamp = it.createdAt, uniqueId = it.value, + serviceProviderEntityId = it.serviceProviderEntityId, providerLogoUrl = it.serviceLogoUrl, ) } diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt index 487a1917..d81308cb 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt @@ -62,7 +62,7 @@ fun DeleteServiceScreen( DeleteServiceContent( providerName = provider?.providerName.orEmpty(), inProgress = uiState.isLoading, - removeService = { viewModel.removeService(provider?.uniqueId) }, + removeService = { viewModel.removeService(provider?.serviceProviderEntityId) }, goBack = goBack ) } @@ -80,7 +80,7 @@ private fun DeleteServiceContent( if (isProcessing && isComplete != null) { val currentGoBack by rememberUpdatedState(goBack) LaunchedEffect(owner) { - isProcessing = true + isProcessing = false currentGoBack() } } diff --git a/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt b/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt index a77903f2..9233e25e 100644 --- a/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt +++ b/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt @@ -26,7 +26,7 @@ class PersonalInfoRepository(private val eduIdApi: EduIdApi) { } suspend fun removeService(serviceId: String): UserDetails? = try { - val response = eduIdApi.removeService(DeleteServiceRequest(serviceId = serviceId)) + val response = eduIdApi.removeService(DeleteServiceRequest(serviceProviderEntityId = serviceId)) if (response.isSuccessful) { response.body() } else { From 6e56378e18bf32f443383f01a713ebd3de1d2ff4 Mon Sep 17 00:00:00 2001 From: Iulia Stana Date: Wed, 12 Apr 2023 13:40:53 +0200 Subject: [PATCH 3/6] Edured-78: Change API call to the latest implementation. Make sure that tokens are being queried and passed for the service to be deleted --- app/build.gradle.kts | 2 +- .../main/kotlin/nl/eduid/di/api/EduIdApi.kt | 3 ++ .../kotlin/nl/eduid/di/model/EduIdModels.kt | 31 +++++++++++++++- .../dataactivity/DataAndActivityData.kt | 20 +++++------ .../dataactivity/DataAndActivityScreen.kt | 25 +++++++++---- .../dataactivity/DataAndActivityViewModel.kt | 23 +++++------- .../dataactivity/DeleteServiceScreen.kt | 7 +++- .../nl/eduid/screens/dataactivity/UiState.kt | 2 +- .../personalinfo/PersonalInfoRepository.kt | 35 ++++++++++++++++++- app/src/main/kotlin/nl/eduid/ui/InfoTab.kt | 13 ++++--- app/src/main/res/values/strings.xml | 2 +- 11 files changed, 118 insertions(+), 45 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dbc37d40..51bf34b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,7 +117,7 @@ android { abortOnError = false } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = "1.4.3" } packagingOptions { resources { diff --git a/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt b/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt index a0dba43d..9b7498b2 100644 --- a/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt +++ b/app/src/main/kotlin/nl/eduid/di/api/EduIdApi.kt @@ -30,6 +30,9 @@ interface EduIdApi { @GET("/mobile/tiqr/sp/start-enrollment") suspend fun startEnrollment(): Response + @GET("/mobile/api/sp/tokens") + suspend fun getTokens(): Response> + @GET("/mobile/api/sp/institution/names") suspend fun getInstitutionName(@Query("schac_home") schac_home: String): Response diff --git a/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt b/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt index a8d1c622..ae374a43 100644 --- a/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt +++ b/app/src/main/kotlin/nl/eduid/di/model/EduIdModels.kt @@ -124,4 +124,33 @@ data class EmailChangeRequest( @JsonClass(generateAdapter = true) data class DeleteServiceRequest( val serviceProviderEntityId: String, -) : Parcelable \ No newline at end of file + val tokens: List, +) : Parcelable + +@Parcelize +@JsonClass(generateAdapter = true) +data class Token( + val id: String, + val type: String, +) : Parcelable + +@Parcelize +@JsonClass(generateAdapter = true) +data class TokenResponse( + val id: String, + val clientName: String, + val clientId: String, + val type: String, + val scopes: List?, +) : Parcelable + +@Parcelize +@JsonClass(generateAdapter = true) +data class Scope(val name: String, val descriptions: Description?) : Parcelable { + fun hasValidDescription(): Boolean = + descriptions != null && (descriptions.en != null || descriptions.nl != null) +} + +@Parcelize +@JsonClass(generateAdapter = true) +data class Description(val en: String?, val nl: String?) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt index b19ed778..c6a7a2b9 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityData.kt @@ -1,14 +1,10 @@ package nl.eduid.screens.dataactivity -data class DataAndActivityData( - val providerList: List? = null, -) { - data class Provider( - val providerName: String, - val createdStamp: Long, - val firstLoginStamp: Long, - val uniqueId: String, - val serviceProviderEntityId: String, - val providerLogoUrl: String, - ) -} \ No newline at end of file +data class ServiceProvider( + val providerName: String, + val createdStamp: Long, + val firstLoginStamp: Long, + val uniqueId: String, + val serviceProviderEntityId: String, + val providerLogoUrl: String, +) \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt index 3fbe47fa..d83020c6 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi +import androidx.lifecycle.compose.collectAsStateWithLifecycle import nl.eduid.ErrorData import nl.eduid.R import nl.eduid.ui.AlertDialogWithSingleButton @@ -20,7 +22,9 @@ import nl.eduid.ui.InfoTab import nl.eduid.ui.getDateTimeString import nl.eduid.ui.theme.ButtonGreen import nl.eduid.ui.theme.EduidAppAndroidTheme +import nl.eduid.util.LogCompositions +@OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun DataAndActivityScreen( viewModel: DataAndActivityViewModel, @@ -41,7 +45,7 @@ fun DataAndActivityScreen( @Composable fun DataAndActivityScreenContent( - dataAndActivity: DataAndActivityData, + data: List, isLoading: Boolean = false, errorData: ErrorData? = null, dismissError: () -> Unit = {}, @@ -71,10 +75,10 @@ fun DataAndActivityScreenContent( ) Spacer(Modifier.height(12.dp)) Text( - style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Start), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Justify, + modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.data_info_subtitle), - modifier = Modifier - .fillMaxWidth() ) Spacer(Modifier.height(12.dp)) if (isLoading) { @@ -86,7 +90,7 @@ fun DataAndActivityScreenContent( .align(alignment = Alignment.CenterHorizontally) ) } else { - dataAndActivity.providerList?.forEachIndexed { index, provider -> + data.forEachIndexed { index, provider -> InfoTab( startIconLargeUrl = provider.providerLogoUrl, title = provider.providerName, @@ -108,6 +112,15 @@ fun DataAndActivityScreenContent( @Composable private fun PreviewDataAndActivityScreenContent() = EduidAppAndroidTheme { DataAndActivityScreenContent( - dataAndActivity = DataAndActivityData(), + data = listOf( + ServiceProvider( + providerName = "Service Provider Name", + createdStamp = 0L, + firstLoginStamp = 0L, + uniqueId = "uniqueId", + serviceProviderEntityId = "serviceprovideridurl", + providerLogoUrl = "dummyImageUrl" + ) + ), ) } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt index 8b38513b..445e935c 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt @@ -67,20 +67,15 @@ class DataAndActivityViewModel @Inject constructor(private val repository: Perso } } - private fun convertToUiData(userDetails: UserDetails): DataAndActivityData { - val providers = userDetails.eduIdPerServiceProvider.values.map { - DataAndActivityData.Provider( - providerName = it.serviceName, - createdStamp = it.createdAt, - firstLoginStamp = it.createdAt, - uniqueId = it.value, - serviceProviderEntityId = it.serviceProviderEntityId, - providerLogoUrl = it.serviceLogoUrl, + private fun convertToUiData(userDetails: UserDetails): List = + userDetails.eduIdPerServiceProvider.values.map { service -> + ServiceProvider( + providerName = service.serviceName, + createdStamp = service.createdAt, + firstLoginStamp = service.createdAt, + uniqueId = service.value, + serviceProviderEntityId = service.serviceProviderEntityId, + providerLogoUrl = service.serviceLogoUrl, ) } - - return DataAndActivityData( - providerList = providers - ) - } } \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt index d81308cb..ee58f11a 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt @@ -56,11 +56,16 @@ fun DeleteServiceScreen( val uiState by viewModel.uiState.observeAsState(UiState()) val provider by remember(index) { derivedStateOf { - uiState.data.providerList?.get(index) + if (uiState.data.isNotEmpty() && index < uiState.data.size) { + uiState.data[index] + } else { + null + } } } DeleteServiceContent( providerName = provider?.providerName.orEmpty(), + isComplete = uiState.isComplete, inProgress = uiState.isLoading, removeService = { viewModel.removeService(provider?.serviceProviderEntityId) }, goBack = goBack diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt index 57204dde..2da17949 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt @@ -3,7 +3,7 @@ package nl.eduid.screens.dataactivity import nl.eduid.ErrorData data class UiState( - val data: DataAndActivityData = DataAndActivityData(), + val data: List = emptyList(), val isLoading: Boolean = false, val errorData: ErrorData? = null, val isComplete: Unit? = null, diff --git a/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt b/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt index 9233e25e..b22fe87c 100644 --- a/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt +++ b/app/src/main/kotlin/nl/eduid/screens/personalinfo/PersonalInfoRepository.kt @@ -3,6 +3,8 @@ package nl.eduid.screens.personalinfo import nl.eduid.di.api.EduIdApi import nl.eduid.di.model.DeleteServiceRequest import nl.eduid.di.model.LinkedAccount +import nl.eduid.di.model.Token +import nl.eduid.di.model.TokenResponse import nl.eduid.di.model.UserDetails import timber.log.Timber @@ -26,7 +28,21 @@ class PersonalInfoRepository(private val eduIdApi: EduIdApi) { } suspend fun removeService(serviceId: String): UserDetails? = try { - val response = eduIdApi.removeService(DeleteServiceRequest(serviceProviderEntityId = serviceId)) + val tokens = getTokensForUser() + val tokensForService = tokens?.filter { token -> + token.clientId == serviceId && token.scopes?.any { scope -> + scope.name != "openid" && scope.hasValidDescription() + } ?: false + } + val tokensRequest = tokensForService?.map { serviceToken -> + Token(serviceToken.id, serviceToken.type) + } ?: emptyList() + + val response = eduIdApi.removeService( + DeleteServiceRequest( + serviceProviderEntityId = serviceId, tokens = tokensRequest + ) + ) if (response.isSuccessful) { response.body() } else { @@ -42,6 +58,23 @@ class PersonalInfoRepository(private val eduIdApi: EduIdApi) { null } + private suspend fun getTokensForUser(): List? = try { + val tokenResponse = eduIdApi.getTokens() + if (tokenResponse.isSuccessful) { + tokenResponse.body() + } else { + Timber.w( + "Failed to remove connection for [${tokenResponse.code()}/${tokenResponse.message()}]${ + tokenResponse.errorBody()?.string() + }" + ) + null + } + } catch (e: Exception) { + Timber.e(e, "Failed to get tokens granted for current user") + null + } + suspend fun removeConnection(linkedAccount: LinkedAccount): UserDetails? = try { val response = eduIdApi.removeConnection(linkedAccount) if (response.isSuccessful) { diff --git a/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt b/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt index f5e8717a..6afdb278 100644 --- a/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt +++ b/app/src/main/kotlin/nl/eduid/ui/InfoTab.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import coil.compose.AsyncImage -import nl.eduid.screens.dataactivity.DataAndActivityData +import nl.eduid.screens.dataactivity.ServiceProvider import nl.eduid.ui.theme.BlueText import nl.eduid.ui.theme.ButtonRed import nl.eduid.ui.theme.InfoTabDarkFill @@ -52,7 +52,7 @@ fun InfoTab( onClick: () -> Unit, enabled: Boolean = true, institutionInfo: PersonalInfo.Companion.InstitutionAccount? = null, - serviceProviderInfo: DataAndActivityData.Provider? = null, + serviceProviderInfo: ServiceProvider? = null, onDeleteButtonClicked: () -> Unit = { }, startIconLargeUrl: String = "", @DrawableRes endIcon: Int = 0, @@ -111,8 +111,7 @@ fun InfoTab( .padding(end = 12.dp) .heightIn(max = 48.dp) .widthIn(max = 48.dp), - - ) + ) } } @@ -186,7 +185,7 @@ fun InfoTab( @Composable private fun InstitutionInfoBlock( institutionInfo: PersonalInfo.Companion.InstitutionAccount, - onDeleteButtonClicked: () -> Unit + onDeleteButtonClicked: () -> Unit, ) { Text( text = "Verified by ${institutionInfo.institution} on ${institutionInfo.createdStamp.getDateString()}", @@ -293,8 +292,8 @@ private fun InstitutionInfoBlock( @Composable private fun serviceProviderBlock( - serviceProviderInfo: DataAndActivityData.Provider, - onDeleteButtonClicked: () -> Unit + serviceProviderInfo: ServiceProvider, + onDeleteButtonClicked: () -> Unit, ): @Composable() (ColumnScope.() -> Unit) = { Text( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index baeb46cf..d9a2592c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,6 @@ Delete login details * Delete service - Are you sure you want to delete your unique pseudonymised eduID for %s and revoke access to your linked accounts?This service might not recognize you the next time you login and all your personal data within this service might be lost. + Are you sure you want to delete your unique pseudonymised eduID for %s and revoke access to your linked accounts? This service might not recognize you the next time you login and all your personal data within this service might be lost. Confirm From 6a9adfe9130a9ff429e4d8d7fea1cba0921427f1 Mon Sep 17 00:00:00 2001 From: Iulia Stana Date: Fri, 14 Apr 2023 13:18:06 +0200 Subject: [PATCH 4/6] Update dependencies --- app/build.gradle.kts | 10 +++++----- build.gradle.kts | 36 ++++++++++------------------------ gradle/libs.versions.toml | 41 +++++++++++++++++++++++++++++---------- settings.gradle.kts | 18 ++++++++++++++--- 4 files changed, 61 insertions(+), 44 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 51bf34b6..9421f11e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -129,11 +129,6 @@ android { dependencies { - repositories { - google() - mavenCentral() - } - implementation(platform("androidx.compose:compose-bom:2022.12.00")) implementation(project(":data")) implementation(project(":core")) @@ -148,6 +143,9 @@ dependencies { implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.view) implementation(libs.androidx.biometric) + val composeBom = platform(libs.androidx.compose.bom) + implementation(composeBom) + implementation("androidx.compose.ui:ui") implementation("androidx.compose.material3:material3") implementation("androidx.compose.ui:ui-tooling-preview") @@ -162,6 +160,8 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.concurrent) implementation(libs.androidx.datastore) + implementation(libs.androidx.lifecycle.runtimeCompose) + implementation(libs.androidx.lifecycle.viewModelCompose) implementation(libs.androidx.lifecycle.common) implementation(libs.androidx.lifecycle.livedata) implementation(libs.androidx.localBroadcastManager) diff --git a/build.gradle.kts b/build.gradle.kts index ddd38293..fbeb9d29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,26 +1,10 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - repositories { - google() - mavenCentral() - } - - dependencies { - classpath(libs.android.gradle) - classpath(libs.kotlin.gradle) - classpath(libs.dagger.hilt.gradle) - classpath(libs.androidx.navigation.gradle) - classpath(libs.google.gms.gradle) - } -} - -subprojects { - repositories { - google() - mavenCentral() - } -} - -tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) -} +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.android.nav.safeargs) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kapt) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.google.gms.gradle) apply false +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 369cabe6..a583c3e2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,21 +4,22 @@ android-sdk-target = "33" android-sdk-min = "24" android-buildTools = "33.0.0" -kotlin = "1.8.0" +androidGradlePlugin = "7.4.2" +hilt = "2.45" +kotlin = "1.8.20" +ksp = "1.8.10-1.0.9" +gmsServicePlugin = "4.3.14" +androidxComposeBom = "2023.04.00" coroutines = "1.6.4" - -hilt = "2.44" navigation = "2.5.3" room = "2.5.0-beta01" -lifecycle = "2.5.1" -camera = "1.3.0-alpha03" - -okhttp = "4.9.2" +lifecycle = "2.6.1" +camera = "1.3.0-alpha05" +okhttp = "4.10.0" retrofit = "2.9.0" moshi = "1.14.0" [libraries] -android-gradle = "com.android.tools.build:gradle:7.4.2" kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -37,7 +38,10 @@ androidx-activity = "androidx.activity:activity-ktx:1.6.1" androidx-appcompat = "androidx.appcompat:appcompat:1.3.1" androidx-autofill = "androidx.autofill:autofill:1.1.0" androidx-biometric = "androidx.biometric:biometric-ktx:1.2.0-alpha05" -androidx-compose-activity = "androidx.activity:activity-compose:1.5.1" +#Compose BOM +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } +androidx-compose-activity = "androidx.activity:activity-compose:1.7.0" androidx-compose-navigation = "androidx.navigation:navigation-compose:2.5.3" androidx-compose-hilt-navigation = "androidx.hilt:hilt-navigation-compose:1.0.0" androidx-concurrent = "androidx.concurrent:concurrent-futures-ktx:1.1.0" @@ -60,6 +64,8 @@ androidx-lifecycle-compiler = { module = "androidx.lifecycle:lifecycle-compiler" androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycle" } androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } +androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } androidx-navigation-gradle = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "navigation" } androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" } @@ -79,7 +85,6 @@ androidx-camera-extensions = "androidx.camera:camera-extensions:1.0.0-alpha30" appauth = "net.openid:appauth:0.11.1" jwtdecode = "com.auth0.android:jwtdecode:2.0.0" -google-gms-gradle = "com.google.gms:google-services:4.3.14" google-android-material = "com.google.android.material:material:1.7.0" google-mlkit-barcode = "com.google.mlkit:barcode-scanning:17.0.2" google-firebase-messaging = "com.google.firebase:firebase-messaging-ktx:23.1.0" @@ -102,3 +107,19 @@ betterLink = "me.saket:better-link-movement-method:2.2.0" timber = "com.jakewharton.timber:timber:5.0.1" junit = "junit:junit:4.13.2" + +# Dependencies of the included build-logic +android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } +kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +android-nav-safeargs = { id = "androidx.navigation.safeargs", version.ref = "navigation" } +google-gms-gradle = { id = "com.google.gms.google-services", version.ref = "gmsServicePlugin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 0fa61d66..8d5a2a3b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,6 +10,18 @@ project(":core").projectDir = File("app-core/core") include(":data") project(":data").projectDir = File("app-core/data") -// Enable Gradle's version catalog support -// https://docs.gradle.org/current/userguide/platforms.html -enableFeaturePreview("VERSION_CATALOGS") + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} From 4f2a8d6c4a7c0428b87566e0b5744a78b185d54b Mon Sep 17 00:00:00 2001 From: Iulia Stana Date: Fri, 14 Apr 2023 15:15:19 +0200 Subject: [PATCH 5/6] Treat delete service as part of the data and activity screen content and not as an independent screen. Ensure compatibility between compose & kotlin compiler --- app/build.gradle.kts | 2 +- .../main/kotlin/nl/eduid/graphs/MainGraph.kt | 22 +----- app/src/main/kotlin/nl/eduid/graphs/Routes.kt | 13 ---- .../dataactivity/DataAndActivityScreen.kt | 74 +++++++++++-------- .../dataactivity/DataAndActivityViewModel.kt | 31 +++++--- ...rviceScreen.kt => DeleteServiceContent.kt} | 62 ++-------------- .../nl/eduid/screens/dataactivity/UiState.kt | 1 + .../firsttimedialog/FirstTimeDialogScreen.kt | 2 +- .../firsttimedialog/LinkAccountContract.kt | 2 +- 9 files changed, 75 insertions(+), 134 deletions(-) rename app/src/main/kotlin/nl/eduid/screens/dataactivity/{DeleteServiceScreen.kt => DeleteServiceContent.kt} (71%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9421f11e..f6e79359 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -117,7 +117,7 @@ android { abortOnError = false } composeOptions { - kotlinCompilerExtensionVersion = "1.4.3" + kotlinCompilerExtensionVersion = "1.4.5" } packagingOptions { resources { diff --git a/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt b/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt index 4fd739b6..a64914d8 100644 --- a/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt +++ b/app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt @@ -19,7 +19,6 @@ import nl.eduid.screens.biometric.EnableBiometricViewModel import nl.eduid.screens.created.RequestEduIdCreatedScreen import nl.eduid.screens.dataactivity.DataAndActivityScreen import nl.eduid.screens.dataactivity.DataAndActivityViewModel -import nl.eduid.screens.dataactivity.DeleteServiceScreen import nl.eduid.screens.deeplinks.DeepLinkScreen import nl.eduid.screens.deeplinks.DeepLinkViewModel import nl.eduid.screens.deleteaccountfirstconfirm.DeleteAccountFirstConfirmScreen @@ -413,26 +412,7 @@ fun MainGraph( val viewModel = hiltViewModel(it) DataAndActivityScreen( viewModel = viewModel, - goBack = { navController.popBackStack() }, - goToConfirmDeleteService = { - navController.navigate( - ConfirmDeleteService.routeForIndex( - it - ) - ) - }, - ) - } - composable( - route = ConfirmDeleteService.routeWithArgs, arguments = ConfirmDeleteService.arguments - ) { entry -> - val viewModel = hiltViewModel(entry) - val index = entry.arguments?.getInt(ConfirmDeleteService.serviceIndexArg, 0) ?: 0 - DeleteServiceScreen( - viewModel = viewModel, - goBack = { navController.popBackStack() }, - index = index, - ) + ) { navController.popBackStack() } } //endregion diff --git a/app/src/main/kotlin/nl/eduid/graphs/Routes.kt b/app/src/main/kotlin/nl/eduid/graphs/Routes.kt index 149ea145..b8204577 100644 --- a/app/src/main/kotlin/nl/eduid/graphs/Routes.kt +++ b/app/src/main/kotlin/nl/eduid/graphs/Routes.kt @@ -66,19 +66,6 @@ object RequestEduIdLinkSent { } } -object ConfirmDeleteService { - private const val route = "confirm_delete_service" - const val serviceIndexArg = "serviceIndexArg" - val routeWithArgs = "$route/{$serviceIndexArg}" - val arguments = listOf(navArgument(serviceIndexArg) { - type = NavType.IntType - nullable = false - defaultValue = 0 - }) - - fun routeForIndex(index: Int) = "$route/$index" -} - sealed class PhoneNumberRecovery(val route: String) { object RequestCode : PhoneNumberRecovery("phone_number_recover") object ConfirmCode : PhoneNumberRecovery("phone_number_confirm_code") { diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt index d83020c6..72fc51f5 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityScreen.kt @@ -1,8 +1,18 @@ package nl.eduid.screens.dataactivity -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.activity.compose.BackHandler +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -12,8 +22,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi -import androidx.lifecycle.compose.collectAsStateWithLifecycle import nl.eduid.ErrorData import nl.eduid.R import nl.eduid.ui.AlertDialogWithSingleButton @@ -22,25 +30,35 @@ import nl.eduid.ui.InfoTab import nl.eduid.ui.getDateTimeString import nl.eduid.ui.theme.ButtonGreen import nl.eduid.ui.theme.EduidAppAndroidTheme -import nl.eduid.util.LogCompositions -@OptIn(ExperimentalLifecycleComposeApi::class) @Composable fun DataAndActivityScreen( viewModel: DataAndActivityViewModel, - goToConfirmDeleteService: (Int) -> Unit, goBack: () -> Unit, -) = EduIdTopAppBar( - onBackClicked = goBack, ) { - val uiState by viewModel.uiState.observeAsState(UiState()) - DataAndActivityScreenContent( - dataAndActivity = uiState.data, - isLoading = uiState.isLoading, - errorData = uiState.errorData, - dismissError = viewModel::clearErrorData, - goToConfirmDeleteService = goToConfirmDeleteService, - ) + BackHandler { viewModel.handleBackNavigation(goBack) } + val dispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher + + EduIdTopAppBar( + onBackClicked = dispatcher::onBackPressed, + ) { + val uiState by viewModel.uiState.observeAsState(UiState()) + + if (uiState.deleteService != null) { + DeleteServiceContent( + providerName = uiState.deleteService?.providerName.orEmpty(), + inProgress = uiState.isLoading, + removeService = { viewModel.removeService(uiState.deleteService?.serviceProviderEntityId) }, + goBack = viewModel::cancelDeleteService + ) + } else { + DataAndActivityScreenContent(data = uiState.data, + isLoading = uiState.isLoading, + errorData = uiState.errorData, + dismissError = viewModel::clearErrorData, + goToConfirmDeleteService = { viewModel.goToDeleteService(it) }) + } + } } @Composable @@ -49,11 +67,10 @@ fun DataAndActivityScreenContent( isLoading: Boolean = false, errorData: ErrorData? = null, dismissError: () -> Unit = {}, - goToConfirmDeleteService: (Int) -> Unit = {}, + goToConfirmDeleteService: (ServiceProvider) -> Unit = {}, ) = Column( verticalArrangement = Arrangement.Bottom, - modifier = Modifier - .verticalScroll(rememberScrollState()) + modifier = Modifier.verticalScroll(rememberScrollState()) ) { if (errorData != null) { AlertDialogWithSingleButton( @@ -66,12 +83,8 @@ fun DataAndActivityScreenContent( Spacer(Modifier.height(36.dp)) Text( style = MaterialTheme.typography.titleLarge.copy( - textAlign = TextAlign.Start, - color = ButtonGreen - ), - text = stringResource(R.string.data_info_title), - modifier = Modifier - .fillMaxWidth() + textAlign = TextAlign.Start, color = ButtonGreen + ), text = stringResource(R.string.data_info_title), modifier = Modifier.fillMaxWidth() ) Spacer(Modifier.height(12.dp)) Text( @@ -90,16 +103,15 @@ fun DataAndActivityScreenContent( .align(alignment = Alignment.CenterHorizontally) ) } else { - data.forEachIndexed { index, provider -> + data.forEach { provider -> InfoTab( startIconLargeUrl = provider.providerLogoUrl, title = provider.providerName, subtitle = stringResource( - R.string.data_info_on_date, - provider.firstLoginStamp.getDateTimeString() + R.string.data_info_on_date, provider.firstLoginStamp.getDateTimeString() ), onClick = { }, - onDeleteButtonClicked = { goToConfirmDeleteService(index) }, + onDeleteButtonClicked = { goToConfirmDeleteService(provider) }, endIcon = R.drawable.chevron_down, serviceProviderInfo = provider, ) diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt index 445e935c..2917794e 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DataAndActivityViewModel.kt @@ -25,10 +25,8 @@ class DataAndActivityViewModel @Inject constructor(private val repository: Perso } else { uiState.postValue( UiState( - isLoading = false, - errorData = ErrorData( - "Failed to load data", - "Could not load activity history" + isLoading = false, errorData = ErrorData( + "Failed to load data", "Could not load activity history" ) ) ) @@ -48,10 +46,7 @@ class DataAndActivityViewModel @Inject constructor(private val repository: Perso val uiData = convertToUiData(userDetails) uiState.postValue( UiState( - isLoading = false, - errorData = null, - data = uiData, - isComplete = Unit + isLoading = false, errorData = null, data = uiData, isComplete = Unit ) ) } else { @@ -59,14 +54,30 @@ class DataAndActivityViewModel @Inject constructor(private val repository: Perso UiState( isLoading = false, errorData = ErrorData( - "Failed to load data", - "Could not load activity history" + "Failed to load data", "Could not load activity history" ), ) ) } } + fun goToDeleteService(provider: ServiceProvider) { + uiState.value = uiState.value?.copy(deleteService = provider) + } + + fun cancelDeleteService() { + uiState.value = uiState.value?.copy(deleteService = null) + } + + fun handleBackNavigation(goBack: () -> Unit) { + val isDeletingService = uiState.value?.deleteService != null + if (isDeletingService) { + cancelDeleteService() + } else { + goBack() + } + } + private fun convertToUiData(userDetails: UserDetails): List = userDetails.eduIdPerServiceProvider.values.map { service -> ServiceProvider( diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceContent.kt similarity index 71% rename from app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt rename to app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceContent.kt index ee58f11a..6a6fa400 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/DeleteServiceContent.kt @@ -17,19 +17,9 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -37,7 +27,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import nl.eduid.R -import nl.eduid.ui.EduIdTopAppBar import nl.eduid.ui.PrimaryButton import nl.eduid.ui.SecondaryButton import nl.eduid.ui.theme.AlertRedBackground @@ -46,50 +35,12 @@ import nl.eduid.ui.theme.EduidAppAndroidTheme import nl.eduid.ui.theme.TextGreen @Composable -fun DeleteServiceScreen( - viewModel: DataAndActivityViewModel, - index: Int, - goBack: () -> Unit = {}, -) = EduIdTopAppBar( - onBackClicked = goBack, -) { - val uiState by viewModel.uiState.observeAsState(UiState()) - val provider by remember(index) { - derivedStateOf { - if (uiState.data.isNotEmpty() && index < uiState.data.size) { - uiState.data[index] - } else { - null - } - } - } - DeleteServiceContent( - providerName = provider?.providerName.orEmpty(), - isComplete = uiState.isComplete, - inProgress = uiState.isLoading, - removeService = { viewModel.removeService(provider?.serviceProviderEntityId) }, - goBack = goBack - ) -} - -@Composable -private fun DeleteServiceContent( +fun DeleteServiceContent( providerName: String, - isComplete: Unit? = null, inProgress: Boolean = false, removeService: () -> Unit = {}, goBack: () -> Unit = {}, ) { - var isProcessing by rememberSaveable { mutableStateOf(false) } - val owner = LocalLifecycleOwner.current - if (isProcessing && isComplete != null) { - val currentGoBack by rememberUpdatedState(goBack) - LaunchedEffect(owner) { - isProcessing = false - currentGoBack() - } - } - Column( modifier = Modifier .fillMaxSize() @@ -148,17 +99,16 @@ private fun DeleteServiceContent( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth() ) { SecondaryButton( - modifier = Modifier.widthIn(min = 140.dp), text = stringResource(R.string.button_cancel), onClick = goBack, + enabled = !inProgress, + modifier = Modifier.widthIn(min = 140.dp), ) PrimaryButton( - modifier = Modifier.widthIn(min = 140.dp), text = stringResource(R.string.button_confirm), - onClick = { - isProcessing = true - removeService() - }, + onClick = removeService, + enabled = !inProgress, + modifier = Modifier.widthIn(min = 140.dp), buttonBackgroundColor = ButtonRed, buttonTextColor = Color.White, ) diff --git a/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt b/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt index 2da17949..4e90a9af 100644 --- a/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt +++ b/app/src/main/kotlin/nl/eduid/screens/dataactivity/UiState.kt @@ -7,4 +7,5 @@ data class UiState( val isLoading: Boolean = false, val errorData: ErrorData? = null, val isComplete: Unit? = null, + val deleteService: ServiceProvider? = null, ) \ No newline at end of file diff --git a/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/FirstTimeDialogScreen.kt b/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/FirstTimeDialogScreen.kt index a14c967e..21856106 100644 --- a/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/FirstTimeDialogScreen.kt +++ b/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/FirstTimeDialogScreen.kt @@ -50,7 +50,7 @@ fun FirstTimeDialogScreen( var isGettingLinkUrl by rememberSaveable { mutableStateOf(false) } var isLinkingStarted by rememberSaveable { mutableStateOf(false) } val launcher = - rememberLauncherForActivityResult(contract = LinkAccountContract(), onResult = { intent -> + rememberLauncherForActivityResult(contract = LinkAccountContract(), onResult = { _ -> if (isLinkingStarted) { isLinkingStarted = false goToAccountLinked() diff --git a/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/LinkAccountContract.kt b/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/LinkAccountContract.kt index cd35ffa9..ad29fba2 100644 --- a/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/LinkAccountContract.kt +++ b/app/src/main/kotlin/nl/eduid/screens/firsttimedialog/LinkAccountContract.kt @@ -12,7 +12,7 @@ class LinkAccountContract : ActivityResultContract() { override fun parseResult(resultCode: Int, intent: Intent?): Intent? { - Timber.e("Received callback from linked account: ${intent?.dataString}") + Timber.d("Received callback from linked account: ${intent?.dataString}") return if (resultCode == RESULT_CANCELED) { null } else { From 3b142e3f7b58dc5588c48ae5629b0f510e186da3 Mon Sep 17 00:00:00 2001 From: Iulia Stana Date: Fri, 14 Apr 2023 15:55:43 +0200 Subject: [PATCH 6/6] Compose 1.4.5 requires Java 17 (temporarily): https://developer.android.com/jetpack/androidx/releases/compose-compiler#version_145_3 Trying to fix "e: java.lang.UnsupportedClassVersionError: ***x/compose/compiler/plugins/kotlin/ComposeComponentRegistrar has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 55.0" happening on github actions. --- .github/workflows/build.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- app/build.gradle.kts | 15 +++++++++------ app/src/main/res/values-nl/strings.xml | 3 +-- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0db6df39..2cf9e8f4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,9 @@ jobs: with: fetch-depth: 0 submodules: recursive - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'temurin' cache: 'gradle' - name: Build nl.eduid Release APK diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4903f58f..cd21dae5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,9 +12,9 @@ jobs: with: fetch-depth: 0 submodules: recursive - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: - java-version: 11 + java-version: 17 distribution: 'temurin' cache: 'gradle' diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6e79359..73b0a9dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,8 +6,8 @@ plugins { id("com.google.dagger.hilt.android") } -if (JavaVersion.current() < JavaVersion.VERSION_11) { - throw GradleException("Please use JDK ${JavaVersion.VERSION_11} or above") +if (JavaVersion.current() < JavaVersion.VERSION_17) { + throw GradleException("Please use JDK ${JavaVersion.VERSION_17} or above") } fun String.runCommand(workingDir: File = file("./")): String { @@ -85,6 +85,8 @@ android { } getByName("debug") { + isMinifyEnabled = true + isShrinkResources = true applicationIdSuffix = ".testing" versionNameSuffix = " DEBUG" buildConfigField("String", "ENV_HOST", "\"https://login.test2.eduid.nl\"") @@ -98,11 +100,12 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + + kotlin { + jvmToolchain(17) } kapt { diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0b272574..e009ad3c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -67,8 +67,7 @@ Check your email To sign in, click the link in the email we sent to - Open Gmail - Open Outlook + Open email client Can’t find it? Check your spam folder. Add a recovery phone number