Skip to content

Commit

Permalink
PR comment: Make AccountLinkedViewModel separate
Browse files Browse the repository at this point in the history
  • Loading branch information
dzolnai committed Nov 21, 2024
1 parent b003c4f commit 726ed6d
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 79 deletions.
3 changes: 2 additions & 1 deletion app/src/main/kotlin/nl/eduid/graphs/MainGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.navigation.navDeepLink
import nl.eduid.graphs.RequestEduIdLinkSent.LOGIN_REASON
import nl.eduid.graphs.RequestEduIdLinkSent.reasonArg
import nl.eduid.screens.accountlinked.AccountLinkedScreen
import nl.eduid.screens.accountlinked.AccountLinkedViewModel
import nl.eduid.screens.accountlinked.ResultAccountLinked
import nl.eduid.screens.biometric.EnableBiometricScreen
import nl.eduid.screens.biometric.EnableBiometricViewModel
Expand Down Expand Up @@ -364,7 +365,7 @@ fun MainGraph(
val fullUri = deepLinkIntent?.data ?: Uri.EMPTY
val result = ResultAccountLinked.fromRedirectUrl(fullUri)

val viewModel = hiltViewModel<PersonalInfoViewModel>(entry)
val viewModel = hiltViewModel<AccountLinkedViewModel>(entry)
AccountLinkedScreen(
viewModel = viewModel,
result = result,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import java.util.Locale

@Composable
fun AccountLinkedScreen(
viewModel: PersonalInfoViewModel,
viewModel: AccountLinkedViewModel,
result: ResultAccountLinked,
continueToHome: () -> Unit,
continueToPersonalInfo: () -> Unit
Expand All @@ -66,9 +66,6 @@ fun AccountLinkedScreen(
continueToPersonalInfo()
}
}
LaunchedEffect(viewModel) {
viewModel.refreshPersonalInfo()
}
Column(
modifier = Modifier
.fillMaxSize()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package nl.eduid.screens.accountlinked

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import nl.eduid.ErrorData
import nl.eduid.R
import nl.eduid.di.assist.DataAssistant
import nl.eduid.di.assist.SaveableResult
import nl.eduid.di.assist.toErrorData
import nl.eduid.graphs.AccountLinked
import nl.eduid.screens.personalinfo.PersonalInfo
import javax.inject.Inject

@HiltViewModel
class AccountLinkedViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val assistant: DataAssistant
) : ViewModel() {
private val _accountLinked: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _errorData: MutableStateFlow<ErrorData?> = MutableStateFlow(null)
private val _isProcessing: MutableStateFlow<Boolean> = MutableStateFlow(false)

val uiState = assistant.observableDetails.map {
when (it) {
is SaveableResult.Success -> {
val personalInfo = PersonalInfo.fromUserDetails(it.data, assistant)
if (it.saveError != null) {
_errorData.emit(it.saveError.toErrorData())
}
UiState(
isLoading = false,
personalInfo = personalInfo
)
}

is SaveableResult.LoadError -> {
_errorData.emit(it.exception.toErrorData())
UiState(isLoading = false)
}

null -> {
_errorData.emit(
ErrorData(
titleId = R.string.ResponseErrors_UnauthorizedTitle_COPY,
messageId = R.string.ResponseErrors_PersonalDetailsRetrieveError_COPY
)
)
UiState(isLoading = false)
}
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3_000),
initialValue = UiState(isLoading = true),
)

val isRegistrationFlow: Boolean = savedStateHandle.get<Boolean>(AccountLinked.isRegistrationFlowArg) ?: false

val accountLinked = _accountLinked.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3_000),
initialValue = false,
)
val errorData = _errorData.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3_000),
initialValue = null,
)
val isProcessing = _isProcessing.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3_000),
initialValue = false,
)

init {
viewModelScope.launch {
refreshPersonalInfo()
}
}

fun clearErrorData() = _errorData.update { null }

fun findLinkedAccount(personalInfo: PersonalInfo?, institutionId: String?): PersonalInfo.InstitutionAccount? {
val completeList = (personalInfo?.linkedInternalAccounts ?: listOf()) + (personalInfo?.linkedExternalAccounts ?: listOf())
completeList.forEach {
if (it.institution == institutionId || it.subjectId == institutionId) {
return it
}
}
return completeList.firstOrNull()
}

fun isFirstLinkedAccount(personalInfo: PersonalInfo): Boolean {
return personalInfo.linkedInternalAccounts.size + personalInfo.linkedExternalAccounts.size < 2
}

fun preferLinkedAccount(linkedAccount: PersonalInfo.InstitutionAccount) {
_isProcessing.update { true }
viewModelScope.launch {
if (assistant.preferLinkedAccount(linkedAccount.updateRequest)) {
_accountLinked.update { true }
} else {
_errorData.update {
ErrorData(
titleId = R.string.ExternalAccountLinkingError_Title_COPY,
messageId = R.string.ExternalAccountLinkingError_Subtitle_COPY
)
}
}
_isProcessing.update { false }
}
}

fun refreshPersonalInfo() {
viewModelScope.launch {
assistant.refreshDetails()
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/kotlin/nl/eduid/screens/accountlinked/UiState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nl.eduid.screens.accountlinked

import androidx.compose.runtime.Stable
import nl.eduid.screens.personalinfo.PersonalInfo

@Stable
data class UiState(
val personalInfo: PersonalInfo = PersonalInfo(),
val isLoading: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package nl.eduid.screens.personalinfo

import android.app.Person
import androidx.compose.runtime.Stable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import nl.eduid.di.assist.DataAssistant
import nl.eduid.di.model.ConfirmedName
import nl.eduid.di.model.LinkedAccountUpdateRequest
import nl.eduid.di.model.SelfAssertedName
import nl.eduid.di.model.UserDetails
import nl.eduid.di.model.mapToPersonalInfo
import java.time.LocalDate

@Stable
Expand All @@ -22,6 +26,7 @@ data class PersonalInfo(
) {
val isVerified = linkedInternalAccounts.isNotEmpty() || linkedExternalAccounts.isNotEmpty()


data class InstitutionAccount(
val subjectId: String,
val role: String?,
Expand All @@ -37,6 +42,35 @@ data class PersonalInfo(
)

companion object {
suspend fun fromUserDetails(userDetails: UserDetails, assistant: DataAssistant): PersonalInfo {
var personalInfo = userDetails.mapToPersonalInfo()
val nameMap = mutableMapOf<String, String>()
for (account in userDetails.linkedAccounts) {
val mappedName = assistant.getInstitutionName(account.schacHomeOrganization)
mappedName?.let {
//If name found, add to list of mapped names
nameMap[account.schacHomeOrganization] = mappedName
//Get name provider from FIRST linked account
if (account.schacHomeOrganization == userDetails.linkedAccounts.firstOrNull()?.schacHomeOrganization) {
personalInfo = personalInfo.copy(
nameProvider = nameMap[account.schacHomeOrganization]
?: personalInfo.nameProvider
)
}
//Update UI data to include mapped institution names
personalInfo =
personalInfo.copy(linkedInternalAccounts = personalInfo.linkedInternalAccounts.map { institution ->
institution.copy(
roleProvider = nameMap[institution.roleProvider]
?: institution.roleProvider
)
}.toImmutableList())
}
}
return personalInfo

}

fun demoData(): PersonalInfo {
return PersonalInfo(
name = "R. van Hamersdonksveer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,12 @@ class PersonalInfoViewModel @Inject constructor(
) : ViewModel() {
private val _errorData: MutableStateFlow<ErrorData?> = MutableStateFlow(null)
private val _isProcessing: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _accountLinked: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val _linkUrl: MutableStateFlow<Intent?> = MutableStateFlow(null)

val isRegistrationFlow: Boolean = savedStateHandle.get<Boolean>(AccountLinked.isRegistrationFlowArg) ?: false

val uiState = assistant.observableDetails.map {
when (it) {
is SaveableResult.Success -> {
val personalInfo = mapUserDetailsToPersonalInfo(it.data)
val personalInfo = PersonalInfo.fromUserDetails(it.data, assistant)
if (it.saveError != null) {
_errorData.emit(it.saveError.toErrorData())
}
Expand Down Expand Up @@ -116,11 +113,6 @@ class PersonalInfoViewModel @Inject constructor(
started = SharingStarted.WhileSubscribed(3_000),
initialValue = false,
)
val accountLinked = _accountLinked.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3_000),
initialValue = false,
)
val linkUrl = _linkUrl.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(3_000),
Expand All @@ -133,34 +125,6 @@ class PersonalInfoViewModel @Inject constructor(

val identityVerificationEnabled = runtimeBehavior.isFeatureEnabled(FeatureFlag.ENABLE_IDENTITY_VERIFICATION)

private suspend fun mapUserDetailsToPersonalInfo(userDetails: UserDetails): PersonalInfo {
var personalInfo = userDetails.mapToPersonalInfo()
val nameMap = mutableMapOf<String, String>()
for (account in userDetails.linkedAccounts) {
val mappedName = assistant.getInstitutionName(account.schacHomeOrganization)
mappedName?.let {
//If name found, add to list of mapped names
nameMap[account.schacHomeOrganization] = mappedName
//Get name provider from FIRST linked account
if (account.schacHomeOrganization == userDetails.linkedAccounts.firstOrNull()?.schacHomeOrganization) {
personalInfo = personalInfo.copy(
nameProvider = nameMap[account.schacHomeOrganization]
?: personalInfo.nameProvider
)
}
//Update UI data to include mapped institution names
personalInfo =
personalInfo.copy(linkedInternalAccounts = personalInfo.linkedInternalAccounts.map { institution ->
institution.copy(
roleProvider = nameMap[institution.roleProvider]
?: institution.roleProvider
)
}.toImmutableList())
}
}
return personalInfo
}

fun clearErrorData() = _errorData.update { null }

fun requestLinkUrl() = viewModelScope.launch {
Expand All @@ -184,41 +148,4 @@ class PersonalInfoViewModel @Inject constructor(
intent.data = Uri.parse(url)
return intent
}

fun findLinkedAccount(personalInfo: PersonalInfo?, institutionId: String?): PersonalInfo.InstitutionAccount? {
val completeList = (personalInfo?.linkedInternalAccounts ?: listOf()) + (personalInfo?.linkedExternalAccounts ?: listOf())
completeList.forEach {
if (it.institution == institutionId || it.subjectId == institutionId) {
return it
}
}
return completeList.firstOrNull()
}

fun isFirstLinkedAccount(personalInfo: PersonalInfo): Boolean {
return personalInfo.linkedInternalAccounts.size + personalInfo.linkedExternalAccounts.size < 2
}

fun preferLinkedAccount(linkedAccount: PersonalInfo.InstitutionAccount) {
_isProcessing.update { true }
viewModelScope.launch {
if (assistant.preferLinkedAccount(linkedAccount.updateRequest)) {
_accountLinked.update { true }
} else {
_errorData.update {
ErrorData(
titleId = R.string.ExternalAccountLinkingError_Title_COPY,
messageId = R.string.ExternalAccountLinkingError_Subtitle_COPY
)
}
}
_isProcessing.update { false }
}
}

fun refreshPersonalInfo() {
viewModelScope.launch {
assistant.refreshDetails()
}
}
}

0 comments on commit 726ed6d

Please sign in to comment.