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
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
@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>(::PermissionPreferences)

val hasTriggeredNotificationPermissionFlow : Flow<Boolean>
var hasTriggeredNotificationPermission by boolPref(key = "HasTriggeredNotificationPermission", defaultValue = false).also {
hasTriggeredNotificationPermissionFlow = it.valueFlow()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -68,26 +66,21 @@ fun MainScreen(
val notificationPermissionState: PermissionState? = if (SDK_INT >= 33) {
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
} else null

var notificationPermissionScreenShown by rememberSaveable { mutableStateOf(false) }
val hasTriggeredNotificationPermission by viewModel.hasTriggeredNotificationPermission.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 showNotificationPermissionScreen = notificationPermissionState != null &&
permissionStatus != PermissionStatus.Granted &&
(!hasTriggeredNotificationPermission || shouldShowRationale)

Comment thread
baillyjamy marked this conversation as resolved.
handleAppStatus(
appStatus = it,
currentDestination = currentDestination,
backStack = backStack,
skipPermission = skipPermission,
onPermissionAsked = {
notificationPermissionScreenShown = true
}
showNotificationPermissionScreen = showNotificationPermissionScreen,
)
}
Comment thread
baillyjamy marked this conversation as resolved.
}
Expand All @@ -101,8 +94,7 @@ private fun handleAppStatus(
appStatus: AppStatus,
currentDestination: NavKey,
backStack: NavBackStack<NavKey>,
skipPermission: Boolean,
onPermissionAsked: () -> Unit,
showNotificationPermissionScreen: Boolean,
) {
val targetDestination = when (appStatus) {
is AppStatus.LoginRequired.NotMigrating -> NavDestination.Onboarding.Start
Expand All @@ -111,11 +103,10 @@ private fun handleAppStatus(
is AppStatus.LoggingIn -> NavDestination.SecuringAccount
is AppStatus.EverythingReady -> NavDestination.Onboarding.Complete
is AppStatus.SetupComplete -> {
if (skipPermission) {
NavDestination.Home
} else {
onPermissionAsked()
if (showNotificationPermissionScreen) {
NavDestination.Permission.Notification
} else {
NavDestination.Home
}
}
is AppStatus.AddingAnAccount -> NavDestination.Onboarding.Start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,4 +40,13 @@ class MainViewModel @Inject constructor(
val appStatus = authenticatorFacade.appStatus

val isAppLocked = appSettingsRepository.getSettings().mapNotNull { it?.isAppLockEnabled }
val hasTriggeredNotificationPermission: StateFlow<Boolean> = flow {
emitAll(PermissionPreferences().hasTriggeredNotificationPermissionFlow)
}.stateIn(viewModelScope, SharingStarted.Lazily, false)

fun onNotificationPermissionTriggered() {
viewModelScope.launch {
PermissionPreferences().hasTriggeredNotificationPermission = true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package com.infomaniak.auth.ui.screen.permission

import android.Manifest
import android.annotation.SuppressLint
import android.os.Build.VERSION.SDK_INT
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
Expand All @@ -35,6 +34,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
Expand All @@ -48,31 +49,52 @@ 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 == PermissionStatus.Denied(false) ||
notificationPermissionState.status is PermissionStatus.Denied && permissionAsked) {
if (!hasTriggeredNotificationPermission) {
viewModel.onNotificationPermissionTriggered()
}
navigateToHome()
Comment thread
baillyjamy marked this conversation as resolved.
}
Comment thread
baillyjamy marked this conversation as resolved.
}

NotificationPermissionScreen(
navigateToHome = navigateToHome,
onPermissionAsked = {
notificationPermissionState?.launchPermissionRequest()
permissionAsked = true
},
)
}

@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun NotificationPermissionScreen(
navigateToHome: () -> Unit,
onPermissionAsked: () -> Unit,
) {
BottomStickyButtonScaffold(
topBar = {
InfomaniakAuthenticatorTopAppBar()
Expand All @@ -82,10 +104,7 @@ fun NotificationPermissionScreen(
LargeButton(
modifier = Modifier.fillMaxWidth(),
title = stringResource(R.string.onboardingNotificationsAuthorisationButton),
onClick = {
notificationPermissionState?.launchPermissionRequest()
permissionAsked = true
}
onClick = onPermissionAsked
)
LargeButton(
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -120,6 +139,7 @@ private fun NotificationPermissionScreenPreview() {
AuthenticatorTheme {
NotificationPermissionScreen(
navigateToHome = {},
onPermissionAsked = {},
)
}
}
Loading