Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 11 additions & 17 deletions app/src/main/kotlin/com/infomaniak/auth/di/ApplicationModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ package com.infomaniak.auth.di

import android.content.Context
import com.infomaniak.auth.BuildConfig
import com.infomaniak.auth.MainApplication
import com.infomaniak.auth.lib.AuthenticatorFacade
import com.infomaniak.auth.lib.models.migration.user.SharedUserProfile
import com.infomaniak.auth.lib.network.interfaces.AuthenticatorBridge
import com.infomaniak.auth.lib.network.interfaces.BreadcrumbType
import com.infomaniak.auth.lib.network.interfaces.CrashReportInterface
import com.infomaniak.auth.lib.network.interfaces.CrashReportLevel
import com.infomaniak.auth.utils.AccountUtils
import com.infomaniak.auth.utils.toMigrationApiToken
import com.infomaniak.auth.utils.toLoginApiToken
import com.infomaniak.auth.utils.toSharedApiToken
import com.infomaniak.auth.utils.toUser
import com.infomaniak.core.auth.room.UserDatabase
import com.infomaniak.core.common.utils.buildUserAgent
Expand All @@ -52,8 +54,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.transform
import javax.inject.Singleton
import com.infomaniak.auth.lib.models.migration.SharedApiToken as MigrationApiToken
import com.infomaniak.lib.login.ApiToken as LoginApiToken
import com.infomaniak.auth.lib.models.migration.SharedApiToken

@Module
@InstallIn(SingletonComponent::class)
Expand Down Expand Up @@ -174,7 +175,7 @@ object ApplicationModule {

override suspend fun getTokenFromCrossAppLogin(
userId: Long
): MigrationApiToken? = crossAppLoginFacade.accountsCheckingState.transform { state ->
): SharedApiToken? = crossAppLoginFacade.accountsCheckingState.transform { state ->
val matchingAccount = state.checkedAccounts.find { it.id == userId }
?: when (state.status) {
AccountsCheckingStatus.Checking -> return@transform // Wait for next emission.
Expand All @@ -184,25 +185,18 @@ object ApplicationModule {
}
if (matchingAccount == null) return@transform emit(null)
val result = crossAppLoginFacade.attemptLogin(listOf(matchingAccount))
emit(result.tokens.singleOrNull()?.toMigrationApiToken())
emit(result.tokens.singleOrNull()?.toSharedApiToken())
}.first()

override suspend fun getTokenFromDatabase(userId: Long): String? {
return accountUtils.getUserById(userId.toInt())?.apiToken?.accessToken
override suspend fun getTokenFromDatabase(userId: Long): SharedApiToken? {
return accountUtils.getUserById(userId.toInt())?.apiToken?.toSharedApiToken()
}

override suspend fun persistTokenForAccount(userId: Long, token: String) {
override suspend fun persistTokenForAccount(userId: Long, token: SharedApiToken) {
MainApplication.userDataCleanableList.forEach { it.resetForUser(userId) }
val dao = UserDatabase.getDatabase().userDao()
val user = accountUtils.getUserById(userId.toInt()) ?: return
dao.update(
user.copy(
apiToken = LoginApiToken(
accessToken = token,
tokenType = user.apiToken.tokenType,
userId = userId.toInt()
)
)
)
dao.update(user.copy(apiToken = token.toLoginApiToken()))
}

override suspend fun persistUserProfile(userProfile: SharedUserProfile) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ val fakeAccounts = persistentListOf(
initials = "JS",
email = "john.smith@ik.me",
avatarUrl = null,
status = Account.Status.LoggedIn,
status = Account.Status.LoggedIn(),
),
Account(
id = 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ private fun SettingsSections(
isRefreshing = true
}

val firstSectionItem = if (accountStatus == Account.Status.LoggedIn) {
val firstSectionItem = if (accountStatus is Account.Status.LoggedIn) {
persistentListOf(
OptionItemType.WithLoader(
stringResId = R.string.refreshPendingLoginsButton,
Expand Down Expand Up @@ -333,7 +333,7 @@ private enum class AccountSecurityConfiguration(

companion object {
fun Account.Status.toSecurityConfiguration(): AccountSecurityConfiguration = when (this) {
Account.Status.LoggedIn -> Secured
is Account.Status.LoggedIn -> Secured
is Account.Status.NotConnected -> Disconnected
else -> PartiallyProtected // TODO: Use secure level to determine the status more precisely
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ fun ActionRequiredCard(
}
}
}
is Account.Status.LoggedIn, is Account.Status.NotConnected.AttemptingToConnect -> Unit
is Account.Status.LoggedIn,
is Account.Status.NotConnected.AttemptingToConnect -> Unit
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import com.infomaniak.auth.ui.components.Avatar
import com.infomaniak.auth.ui.components.StatusCard
import com.infomaniak.auth.ui.components.StatusCardVariant
import com.infomaniak.auth.ui.previewparameter.fakeAccountPairs
import com.infomaniak.auth.ui.screen.accountlist.AccountListViewModel.AccountListUiState
import com.infomaniak.auth.ui.screen.accountlist.AccountSecurityLevel.Companion.toAccountSecurityLevel
import com.infomaniak.auth.ui.theme.AppDimens.DefaultCornerRadius
import com.infomaniak.auth.ui.theme.AuthenticatorTheme
Expand All @@ -86,6 +87,7 @@ fun AccountListScreen(
uiState = { state },
onAccountClicked = onAccountClicked,
onChallengesRefreshRequested = viewModel::refreshChallenges,
onUserProfilesRefreshRequested = viewModel::refreshUserProfiles,
)
}
is AccountListUiState.Loading -> Unit
Expand All @@ -97,6 +99,7 @@ fun AccountListScreen(
uiState: () -> AccountListUiState.Success,
onAccountClicked: (Account) -> Unit,
onChallengesRefreshRequested: () -> Unit,
onUserProfilesRefreshRequested: () -> Unit,
modifier: Modifier = Modifier
) {
val state = uiState()
Expand All @@ -122,6 +125,7 @@ fun AccountListScreen(
onRefresh = {
isRefreshing = true
onChallengesRefreshRequested()
onUserProfilesRefreshRequested()
},
) {
Column(
Expand Down Expand Up @@ -229,11 +233,18 @@ private enum class AccountSecurityLevel(val iconResId: Int, val iconTint: @Compo
Danger(iconResId = R.drawable.shield_exclamation_mark, iconTint = { AuthenticatorTheme.customColors.iconTintWarning });

companion object {
fun Account.Status.toAccountSecurityLevel() = when (this) {
Account.Status.LoggedIn -> Secured
fun Account.Status.toAccountSecurityLevel(): AccountSecurityLevel = when (this) {
is Account.Status.LoggedIn -> accountSecurityLevelForScore(securityScore)
is Account.Status.NotConnected -> Danger //TODO: Shouldn't this show the exclamation mark too?
else -> Warning // TODO: Use secure level to determine the status more precisely
}

private fun accountSecurityLevelForScore(securityScore: Int?): AccountSecurityLevel {
return when (securityScore) {
5 -> Secured
else -> Warning
}
}
}
}

Expand All @@ -247,6 +258,7 @@ private fun AccountListScreenPreview() {
uiState = { AccountListUiState.Success(fakeAccountPairs) },
onAccountClicked = {},
onChallengesRefreshRequested = {},
onUserProfilesRefreshRequested = {},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,21 @@ class AccountListViewModel @Inject constructor(
viewModelScope.launch {
authenticatorFacade.accounts.first()
.filter { account ->
account.status == Account.Status.LoggedIn
account.status is Account.Status.LoggedIn
}
.forEach { account ->
twoFactorAuthManager.refreshChallengeNow(account.id)
}
}
}
}

@Immutable
sealed interface AccountListUiState {
data object Loading : AccountListUiState
data class Success(val accountPairs: ImmutableList<Pair<Account, User?>>) : AccountListUiState
fun refreshUserProfiles() {
authenticatorFacade.refreshUserProfiles()
}

@Immutable
sealed interface AccountListUiState {
data object Loading : AccountListUiState
data class Success(val accountPairs: ImmutableList<Pair<Account, User?>>) : AccountListUiState
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.NavBackStack
Expand All @@ -40,6 +42,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.infomaniak.auth.lib.Account
import com.infomaniak.auth.lib.AppStatus
import com.infomaniak.auth.ui.navigation.NavDestination
import com.infomaniak.auth.ui.navigation.baseEntryProvider
Expand Down Expand Up @@ -85,10 +88,17 @@ fun MainScreen(
}
}

var showPasswordChangedDialogFor: Account? by remember { mutableStateOf(null) }

LaunchedEffect(Unit) {
viewModel.accountsWithPasswordUpdate.collect { accounts ->
accounts.firstOrNull()?.let { showPasswordChangedDialogFor = it }
}
}

MainScreen(backStack, entryDecorators)
}


@OptIn(ExperimentalPermissionsApi::class)
private fun handleAppStatus(
appStatus: AppStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ package com.infomaniak.auth.ui.screen.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.infomaniak.auth.data.preferences.PermissionPreferences
import com.infomaniak.auth.lib.Account
import com.infomaniak.auth.lib.AuthenticatorFacade
import com.infomaniak.auth.lib.repository.AppSettingsRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand All @@ -39,6 +42,15 @@ class MainViewModel @Inject constructor(
) : ViewModel() {
val appStatus = authenticatorFacade.appStatus

val accountsWithPasswordUpdate: Flow<List<Account>> = authenticatorFacade.accounts.map { accounts ->
accounts.filter {
when (val status = it.status) {
is Account.Status.LoggedIn if (status.passwordChangedAck != null) -> true
else -> false
}
}
}

val isAppLocked = appSettingsRepository.getSettings().mapNotNull { it?.isAppLockEnabled }
val hasTriggeredNotificationPermission: StateFlow<Boolean> = flow {
emitAll(PermissionPreferences().hasTriggeredNotificationPermissionFlow)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.infomaniak.auth.lib.AppStatus
import com.infomaniak.auth.lib.AuthenticatorFacade
import com.infomaniak.auth.lib.matomo.MatomoName
import com.infomaniak.auth.utils.AccountUtils
import com.infomaniak.auth.utils.toSharedUser
import com.infomaniak.core.auth.models.UserLoginResult
import com.infomaniak.core.auth.models.user.User
import com.infomaniak.core.auth.utils.LoginUtils
Expand Down Expand Up @@ -70,15 +71,7 @@ class OnboardingStartViewModel @Inject constructor(
}

private suspend fun addUserToAuthenticatorDB(user: User) {
val connectedAccount = Account(
id = user.id.toLong(),
fullName = "${user.firstname} ${user.lastname}",
initials = user.getInitials(),
email = user.email,
avatarUrl = user.avatar,
status = Account.Status.LoggedIn,
)
authenticatorFacade.addAccounts(listOf(connectedAccount))
authenticatorFacade.addAccounts(listOf(user.toSharedUser()))
}

suspend fun connectSelectedAccounts(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ class AccountUtils @Inject constructor(
suspend fun isUserConnected(): Boolean = users.first().isNotEmpty()

suspend fun getUserById(id: Int): User? = userDao.findById(id)
suspend fun getUsersById(userIds: IntArray): List<User> = userDao.loadAllByIds(userIds)
}
Loading
Loading