From 252c0f11952ba0ce05cc076b7ba35a5615aff788 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Thu, 30 Apr 2026 15:23:50 +0200 Subject: [PATCH 1/3] fix: Ask permission only first open and rationale == true --- .../data/preferences/PermissionPreferences.kt | 34 +++++++++++++++++++ .../auth/ui/screen/main/MainScreen.kt | 28 +++++++-------- .../auth/ui/screen/main/MainViewModel.kt | 17 ++++++++++ .../NotificationPermissionScreen.kt | 1 - 4 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt diff --git a/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt b/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt new file mode 100644 index 00000000..3a110c15 --- /dev/null +++ b/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt @@ -0,0 +1,34 @@ +/* + * Infomaniak Authenticator - Android + * Copyright (C) 2026 Infomaniak Network SA + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +@file:OptIn(ExperimentalSplittiesApi::class) + +package com.infomaniak.auth.data.preferences + +import kotlinx.coroutines.flow.Flow +import splitties.experimental.ExperimentalSplittiesApi +import splitties.preferences.Preferences +import splitties.preferences.SuspendPrefsAccessor + +class PermissionPreferences private constructor(): Preferences(name = "PermissionPreferences") { + companion object : SuspendPrefsAccessor(::PermissionPreferences) + + val isFirstTimeNotificationPermissionGrantedFlow : Flow + var isFirstTimeNotificationPermissionAsked by boolPref(key = "IsFirstTimeNotificationPermissionAsked", defaultValue = false).also { + isFirstTimeNotificationPermissionGrantedFlow = it.valueFlow() + } +} diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt index 1dc76079..165eaf63 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt @@ -24,10 +24,8 @@ 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.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavBackStack import androidx.navigation3.runtime.NavEntryDecorator @@ -68,25 +66,23 @@ fun MainScreen( val notificationPermissionState: PermissionState? = if (SDK_INT >= 33) { rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) } else null - - var notificationPermissionScreenShown by rememberSaveable { mutableStateOf(false) } + val isFirstTimeNotificationPermissionAsked by viewModel.isFirstTimeNotificationPermissionAsked.collectAsStateWithLifecycle() LaunchedEffect(viewModel.appStatus) { viewModel.appStatus.collect { val permissionStatus = notificationPermissionState?.status - val isPermanentlyDenied = (permissionStatus as? PermissionStatus.Denied)?.shouldShowRationale == false - val isPermissionNotRequired = with(permissionStatus) { - (this == null || this == PermissionStatus.Granted || !isPermanentlyDenied) - } - val skipPermission = notificationPermissionScreenShown && isPermissionNotRequired + val shouldShowRationale = (permissionStatus as? PermissionStatus.Denied)?.shouldShowRationale == true + val askNotificationPermission = isFirstTimeNotificationPermissionAsked || shouldShowRationale handleAppStatus( appStatus = it, currentDestination = currentDestination, backStack = backStack, - skipPermission = skipPermission, + askNotificationPermission = askNotificationPermission, onPermissionAsked = { - notificationPermissionScreenShown = true + if (isFirstTimeNotificationPermissionAsked) { + viewModel.onFirstTimeNotificationPermissionAsked() + } } ) } @@ -101,7 +97,7 @@ private fun handleAppStatus( appStatus: AppStatus, currentDestination: NavKey, backStack: NavBackStack, - skipPermission: Boolean, + askNotificationPermission: Boolean, onPermissionAsked: () -> Unit, ) { val targetDestination = when (appStatus) { @@ -111,11 +107,11 @@ private fun handleAppStatus( is AppStatus.LoggingIn -> NavDestination.SecuringAccount is AppStatus.EverythingReady -> NavDestination.Onboarding.Complete is AppStatus.SetupComplete -> { - if (skipPermission) { - NavDestination.Home - } else { + if (askNotificationPermission) { onPermissionAsked() NavDestination.Permission.Notification + } else { + NavDestination.Home } } is AppStatus.AddingAnAccount -> NavDestination.Onboarding.Start diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt index 501f0496..c565a6d5 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt @@ -18,10 +18,18 @@ 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.AuthenticatorFacade import com.infomaniak.auth.lib.repository.AppSettingsRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel @@ -32,4 +40,13 @@ class MainViewModel @Inject constructor( val appStatus = authenticatorFacade.appStatus val isAppLocked = appSettingsRepository.getSettings().mapNotNull { it?.isAppLockEnabled } + val isFirstTimeNotificationPermissionAsked: StateFlow = flow { + emitAll(PermissionPreferences().isFirstTimeNotificationPermissionGrantedFlow) + }.stateIn(viewModelScope, SharingStarted.Lazily, false) + + fun onFirstTimeNotificationPermissionAsked() { + viewModelScope.launch { + PermissionPreferences().isFirstTimeNotificationPermissionAsked = true + } + } } diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt index 3c8f15a6..3daa5df7 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt @@ -67,7 +67,6 @@ fun NotificationPermissionScreen( LaunchedEffect(notificationPermissionState?.status) { if (notificationPermissionState == null || notificationPermissionState.status == PermissionStatus.Granted || - notificationPermissionState.status == PermissionStatus.Denied(false) || notificationPermissionState.status is PermissionStatus.Denied && permissionAsked) { navigateToHome() } From ca171d0b45695c2363c22eff7cf7fb3ba05cdf07 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Thu, 30 Apr 2026 16:25:40 +0200 Subject: [PATCH 2/3] fix: Clean logic --- .../data/preferences/PermissionPreferences.kt | 6 +-- .../auth/ui/screen/main/MainScreen.kt | 17 +++----- .../auth/ui/screen/main/MainViewModel.kt | 8 ++-- .../NotificationPermissionScreen.kt | 39 +++++++++++++++---- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt b/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt index 3a110c15..09cf5a65 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/data/preferences/PermissionPreferences.kt @@ -27,8 +27,8 @@ import splitties.preferences.SuspendPrefsAccessor class PermissionPreferences private constructor(): Preferences(name = "PermissionPreferences") { companion object : SuspendPrefsAccessor(::PermissionPreferences) - val isFirstTimeNotificationPermissionGrantedFlow : Flow - var isFirstTimeNotificationPermissionAsked by boolPref(key = "IsFirstTimeNotificationPermissionAsked", defaultValue = false).also { - isFirstTimeNotificationPermissionGrantedFlow = it.valueFlow() + val hasTriggeredNotificationPermissionFlow : Flow + var hasTriggeredNotificationPermission by boolPref(key = "HasTriggeredNotificationPermission", defaultValue = false).also { + hasTriggeredNotificationPermissionFlow = it.valueFlow() } } diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt index 165eaf63..fc970331 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt @@ -66,24 +66,19 @@ fun MainScreen( val notificationPermissionState: PermissionState? = if (SDK_INT >= 33) { rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) } else null - val isFirstTimeNotificationPermissionAsked by viewModel.isFirstTimeNotificationPermissionAsked.collectAsStateWithLifecycle() + val hasTriggeredNotificationPermission by viewModel.hasTriggeredNotificationPermission.collectAsStateWithLifecycle() LaunchedEffect(viewModel.appStatus) { viewModel.appStatus.collect { val permissionStatus = notificationPermissionState?.status val shouldShowRationale = (permissionStatus as? PermissionStatus.Denied)?.shouldShowRationale == true - val askNotificationPermission = isFirstTimeNotificationPermissionAsked || shouldShowRationale + val showNotificationPermissionScreen = permissionStatus != PermissionStatus.Granted && (!hasTriggeredNotificationPermission || shouldShowRationale) handleAppStatus( appStatus = it, currentDestination = currentDestination, backStack = backStack, - askNotificationPermission = askNotificationPermission, - onPermissionAsked = { - if (isFirstTimeNotificationPermissionAsked) { - viewModel.onFirstTimeNotificationPermissionAsked() - } - } + showNotificationPermissionScreen = showNotificationPermissionScreen, ) } } @@ -97,8 +92,7 @@ private fun handleAppStatus( appStatus: AppStatus, currentDestination: NavKey, backStack: NavBackStack, - askNotificationPermission: Boolean, - onPermissionAsked: () -> Unit, + showNotificationPermissionScreen: Boolean, ) { val targetDestination = when (appStatus) { is AppStatus.LoginRequired.NotMigrating -> NavDestination.Onboarding.Start @@ -107,8 +101,7 @@ private fun handleAppStatus( is AppStatus.LoggingIn -> NavDestination.SecuringAccount is AppStatus.EverythingReady -> NavDestination.Onboarding.Complete is AppStatus.SetupComplete -> { - if (askNotificationPermission) { - onPermissionAsked() + if (showNotificationPermissionScreen) { NavDestination.Permission.Notification } else { NavDestination.Home diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt index c565a6d5..d490a806 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainViewModel.kt @@ -40,13 +40,13 @@ class MainViewModel @Inject constructor( val appStatus = authenticatorFacade.appStatus val isAppLocked = appSettingsRepository.getSettings().mapNotNull { it?.isAppLockEnabled } - val isFirstTimeNotificationPermissionAsked: StateFlow = flow { - emitAll(PermissionPreferences().isFirstTimeNotificationPermissionGrantedFlow) + val hasTriggeredNotificationPermission: StateFlow = flow { + emitAll(PermissionPreferences().hasTriggeredNotificationPermissionFlow) }.stateIn(viewModelScope, SharingStarted.Lazily, false) - fun onFirstTimeNotificationPermissionAsked() { + fun onNotificationPermissionTriggered() { viewModelScope.launch { - PermissionPreferences().isFirstTimeNotificationPermissionAsked = true + PermissionPreferences().hasTriggeredNotificationPermission = true } } } diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt index 3daa5df7..21f9cbef 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt @@ -18,8 +18,8 @@ package com.infomaniak.auth.ui.screen.permission import android.Manifest -import android.annotation.SuppressLint import android.os.Build.VERSION.SDK_INT +import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -35,6 +35,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionStatus @@ -48,30 +50,53 @@ import com.infomaniak.auth.ui.components.LargeButton import com.infomaniak.auth.ui.components.TitleAndDescription import com.infomaniak.auth.ui.images.AppImages import com.infomaniak.auth.ui.images.illus.bannerNotification.BannerNotification +import com.infomaniak.auth.ui.screen.main.MainViewModel import com.infomaniak.auth.ui.theme.AuthenticatorTheme import com.infomaniak.core.ui.compose.bottomstickybuttonscaffolds.BottomStickyButtonScaffold import com.infomaniak.core.ui.compose.margin.Margin import com.infomaniak.core.ui.compose.preview.PreviewSmallWindow @OptIn(ExperimentalPermissionsApi::class) -@SuppressLint("ComposeModifierMissing") @Composable fun NotificationPermissionScreen( - navigateToHome: () -> Unit + navigateToHome: () -> Unit, + viewModel: MainViewModel = hiltViewModel() ) { - var permissionAsked by remember { mutableStateOf(false) } + val hasTriggeredNotificationPermission by viewModel.hasTriggeredNotificationPermission.collectAsStateWithLifecycle() + val notificationPermissionState: PermissionState? = if (SDK_INT >= 33) { rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) } else null + var permissionAsked by remember { mutableStateOf(false) } + LaunchedEffect(notificationPermissionState?.status) { if (notificationPermissionState == null || notificationPermissionState.status == PermissionStatus.Granted || notificationPermissionState.status is PermissionStatus.Denied && permissionAsked) { + if (!hasTriggeredNotificationPermission) { + Log.v("Jamy", "NotificationPermissionScreen: ici") + viewModel.onNotificationPermissionTriggered() + } navigateToHome() } } + NotificationPermissionScreen( + navigateToHome = navigateToHome, + onPermissionAsked = { + notificationPermissionState?.launchPermissionRequest() + permissionAsked = true + }, + ) +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun NotificationPermissionScreen( + navigateToHome: () -> Unit, + onPermissionAsked: () -> Unit, +) { BottomStickyButtonScaffold( topBar = { InfomaniakAuthenticatorTopAppBar() @@ -81,10 +106,7 @@ fun NotificationPermissionScreen( LargeButton( modifier = Modifier.fillMaxWidth(), title = stringResource(R.string.onboardingNotificationsAuthorisationButton), - onClick = { - notificationPermissionState?.launchPermissionRequest() - permissionAsked = true - } + onClick = onPermissionAsked ) LargeButton( modifier = Modifier.fillMaxWidth(), @@ -119,6 +141,7 @@ private fun NotificationPermissionScreenPreview() { AuthenticatorTheme { NotificationPermissionScreen( navigateToHome = {}, + onPermissionAsked = {}, ) } } From a3d886b949056bf6d30247ef1b20ba1ad8ca05f7 Mon Sep 17 00:00:00 2001 From: Jamy Bailly Date: Thu, 30 Apr 2026 16:44:02 +0200 Subject: [PATCH 3/3] fix: Remove log and verify can ask permission on android < 33 --- .../kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt | 4 +++- .../auth/ui/screen/permission/NotificationPermissionScreen.kt | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt index fc970331..c8f51a66 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/main/MainScreen.kt @@ -72,7 +72,9 @@ fun MainScreen( viewModel.appStatus.collect { val permissionStatus = notificationPermissionState?.status val shouldShowRationale = (permissionStatus as? PermissionStatus.Denied)?.shouldShowRationale == true - val showNotificationPermissionScreen = permissionStatus != PermissionStatus.Granted && (!hasTriggeredNotificationPermission || shouldShowRationale) + val showNotificationPermissionScreen = notificationPermissionState != null && + permissionStatus != PermissionStatus.Granted && + (!hasTriggeredNotificationPermission || shouldShowRationale) handleAppStatus( appStatus = it, diff --git a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt index 21f9cbef..10a19fce 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/ui/screen/permission/NotificationPermissionScreen.kt @@ -19,7 +19,6 @@ package com.infomaniak.auth.ui.screen.permission import android.Manifest import android.os.Build.VERSION.SDK_INT -import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight @@ -75,7 +74,6 @@ fun NotificationPermissionScreen( notificationPermissionState.status == PermissionStatus.Granted || notificationPermissionState.status is PermissionStatus.Denied && permissionAsked) { if (!hasTriggeredNotificationPermission) { - Log.v("Jamy", "NotificationPermissionScreen: ici") viewModel.onNotificationPermissionTriggered() } navigateToHome()