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