Add LocalNetworkPermissionGate with rationale dialog

This commit is contained in:
ganfra
2026-07-07 21:37:48 +02:00
parent e9abed9178
commit c2e8912fe2
4 changed files with 168 additions and 0 deletions
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.localnetwork
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun LocalNetworkPermissionDialogView(
dialog: LocalNetworkPermissionDialog,
onSubmit: () -> Unit,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
when (dialog) {
LocalNetworkPermissionDialog.None -> Unit
LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog(
title = stringResource(CommonStrings.screen_local_network_opt_in_title),
content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle),
submitText = stringResource(CommonStrings.dialog_allow_access),
cancelText = stringResource(CommonStrings.action_not_now),
onSubmitClick = onSubmit,
onDismiss = onDismiss,
modifier = modifier,
)
LocalNetworkPermissionDialog.Settings -> ConfirmationDialog(
title = stringResource(CommonStrings.screen_local_network_opt_in_title),
content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle),
submitText = stringResource(CommonStrings.action_open_settings),
cancelText = stringResource(CommonStrings.action_not_now),
onSubmitClick = onSubmit,
onDismiss = onDismiss,
modifier = modifier,
)
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.localnetwork
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject
import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor
import io.element.android.libraries.permissions.api.PermissionsEvent
import io.element.android.libraries.permissions.api.PermissionsPresenter
import kotlinx.coroutines.launch
@Inject
class LocalNetworkPermissionGate(
private val advisor: LocalNetworkPermissionAdvisor,
permissionsPresenterFactory: PermissionsPresenter.Factory,
) {
private val permissionsPresenter: PermissionsPresenter =
permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK)
@Composable
fun <T : Any> present(
urlOf: (T) -> String,
onProceed: suspend (T) -> Unit,
): LocalNetworkPermissionGateState<T> {
val coroutineScope = rememberCoroutineScope()
val permissionsState = permissionsPresenter.present()
var pendingSubmit by remember { mutableStateOf<T?>(null) }
val urlOf by rememberUpdatedState(urlOf)
val onProceed by rememberUpdatedState(onProceed)
LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) {
val pending = pendingSubmit
if (pending != null && permissionsState.permissionGranted) {
coroutineScope.launch { onProceed(pending) }
pendingSubmit = null
}
}
val dialog by rememberUpdatedState(
when {
pendingSubmit == null -> LocalNetworkPermissionDialog.None
permissionsState.permissionGranted -> LocalNetworkPermissionDialog.None
permissionsState.shouldShowRationale || !permissionsState.permissionAlreadyAsked -> LocalNetworkPermissionDialog.Rationale
else -> LocalNetworkPermissionDialog.Settings
}
)
fun submit(value: T) {
coroutineScope.launch {
if (advisor.shouldRequestPermissionFor(urlOf(value))) {
pendingSubmit = value
} else {
onProceed(value)
}
}
}
fun requestPermission() {
when (dialog) {
LocalNetworkPermissionDialog.Settings -> {
permissionsState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog)
}
LocalNetworkPermissionDialog.Rationale -> {
permissionsState.eventSink(PermissionsEvent.ForceRequestPermissions)
}
else -> Unit
}
}
fun abort() {
pendingSubmit = null
}
return LocalNetworkPermissionGateState(
dialog = dialog,
submit = ::submit,
requestPermission = ::requestPermission,
abort = ::abort,
)
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.login.impl.localnetwork
data class LocalNetworkPermissionGateState<T>(
val dialog: LocalNetworkPermissionDialog,
val submit: (T) -> Unit,
val requestPermission: () -> Unit,
val abort: () -> Unit,
)
/**
* Which rationale dialog (if any) should be rendered on top of the caller's UI.
*
* [Rationale] is shown before the runtime prompt so the user knows why the permission is needed.
* [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied).
*/
enum class LocalNetworkPermissionDialog {
None,
Rationale,
Settings,
}
@@ -8,4 +8,6 @@
<resources>
<string name="action_mark_as_read">"Mark as read"</string>
<string name="a11y_jump_to_unread_messages">"Jump to first unread message"</string>
<string name="screen_local_network_opt_in_title">Allow access to local network</string>
<string name="screen_local_network_opt_in_subtitle">Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network.</string>
</resources>