mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
refactor: shared device-credential prompt for key access
rememberDeviceAuthenticator (commonsUI) replaces the app's authenticate() helper: Android keeps BiometricPrompt with the keyguard fallback in its actual; desktop and iOS approve at once for now, as Android does on a device without a screen lock. UpdateZapAmountDialog no longer carries the prompt code, and AccountBackupScreen asks through the port. commonsUI's Android source set takes androidx.biometric (Apache-2.0, already shipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
@@ -20,14 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
@@ -59,7 +51,6 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
@@ -86,10 +77,6 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer_with_error
|
||||
import com.vitorpamplona.amethyst.commons.resources.new_amount_in_sats
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts
|
||||
import com.vitorpamplona.amethyst.commons.resources.quick_zap_amounts_explainer
|
||||
@@ -121,10 +108,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.util.showAmount
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.getFragmentActivity
|
||||
import com.vitorpamplona.quartz.nip57Zaps.ZapReceiptEvent
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@@ -449,118 +434,3 @@ private fun ZapAmountPresetChip(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels the device-credential and biometric prompts need. Resolved in
|
||||
* composition and passed down because [authenticate] runs from an onClick
|
||||
* lambda and from Android's own biometric callbacks, neither of which can
|
||||
* reach a Compose resource.
|
||||
*/
|
||||
@Immutable
|
||||
data class AuthPromptLabels(
|
||||
val appName: String,
|
||||
val failedTitle: String,
|
||||
val failedExplainer: String,
|
||||
val failedExplainerWithError: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun rememberAuthPromptLabels(): AuthPromptLabels =
|
||||
AuthPromptLabels(
|
||||
appName = stringRes(Res.string.app_name),
|
||||
failedTitle = stringRes(Res.string.biometric_authentication_failed),
|
||||
failedExplainer = stringRes(Res.string.biometric_authentication_failed_explainer),
|
||||
failedExplainerWithError = stringRes(Res.string.biometric_authentication_failed_explainer_with_error),
|
||||
)
|
||||
|
||||
fun authenticate(
|
||||
title: String,
|
||||
context: Context,
|
||||
labels: AuthPromptLabels,
|
||||
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
|
||||
onApproved: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
val fragmentContext = context.getFragmentActivity()!!
|
||||
val keyguardManager =
|
||||
fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
|
||||
if (!keyguardManager.isDeviceSecure) {
|
||||
onApproved()
|
||||
return
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun keyguardPrompt() {
|
||||
val intent =
|
||||
keyguardManager.createConfirmDeviceCredentialIntent(
|
||||
labels.appName,
|
||||
title,
|
||||
)
|
||||
|
||||
keyguardLauncher.launch(intent)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
keyguardPrompt()
|
||||
return
|
||||
}
|
||||
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val authenticators =
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
|
||||
val promptInfo =
|
||||
BiometricPrompt.PromptInfo
|
||||
.Builder()
|
||||
.setTitle(labels.appName)
|
||||
.setSubtitle(title)
|
||||
.setAllowedAuthenticators(authenticators)
|
||||
.build()
|
||||
|
||||
val biometricPrompt =
|
||||
BiometricPrompt(
|
||||
fragmentContext,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence,
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
|
||||
keyguardPrompt()
|
||||
}
|
||||
|
||||
BiometricPrompt.ERROR_LOCKOUT -> {
|
||||
keyguardPrompt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
onError(
|
||||
labels.failedTitle,
|
||||
labels.failedExplainerWithError.format(errString.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onError(labels.failedTitle, labels.failedExplainer)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onApproved()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
when (biometricManager.canAuthenticate(authenticators)) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo)
|
||||
else -> keyguardPrompt()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-33
@@ -20,16 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.os.PersistableBundle
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -136,14 +132,13 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.privacylock.rememberDeviceAuthenticator
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.BackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.qrcode.QrCodeDrawer
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.note.authenticate
|
||||
import com.vitorpamplona.amethyst.ui.note.rememberAuthPromptLabels
|
||||
import com.vitorpamplona.quartz.nip19Bech32.toNsec
|
||||
import com.vitorpamplona.quartz.nip49PrivKeyEnc.Nip49
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -701,13 +696,6 @@ private class KeyAccessGate {
|
||||
var isUnlocked by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
/**
|
||||
* The action waiting on the keyguard fallback activity to return. Only a new request or
|
||||
* that activity's result replaces it: the activity stops this screen (so ON_STOP must not
|
||||
* clear it), and a wrong fingerprint before the lockout fallback is not a final failure.
|
||||
*/
|
||||
var pending: (() -> Unit)? = null
|
||||
|
||||
var prompt: ((onApproved: () -> Unit) -> Unit)? = null
|
||||
|
||||
fun withAccess(action: () -> Unit) {
|
||||
@@ -728,32 +716,15 @@ private class KeyAccessGate {
|
||||
|
||||
@Composable
|
||||
private fun rememberKeyAccessGate(accountViewModel: AccountViewModel): KeyAccessGate {
|
||||
val context = LocalContext.current
|
||||
val authLabels = rememberAuthPromptLabels()
|
||||
val authenticator = rememberDeviceAuthenticator()
|
||||
val authTitle = stringRes(Res.string.backup_keys)
|
||||
val gate = remember { KeyAccessGate() }
|
||||
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
val action = gate.pending
|
||||
gate.pending = null
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
action?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
gate.prompt = { onApproved ->
|
||||
gate.pending = onApproved
|
||||
authenticate(
|
||||
authenticator.authenticate(
|
||||
title = authTitle,
|
||||
context = context,
|
||||
labels = authLabels,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
gate.pending = null
|
||||
onApproved()
|
||||
},
|
||||
onApproved = onApproved,
|
||||
onError = { title, message -> accountViewModel.toastManager.toast(title, message) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -216,6 +216,8 @@ kotlin {
|
||||
implementation(libs.androidx.activity.compose)
|
||||
// Camera permission prompt for TakePicture. Apache-2.0, already in the app.
|
||||
implementation(libs.accompanist.permissions)
|
||||
// BiometricPrompt behind rememberDeviceAuthenticator. Apache-2.0, already in the app.
|
||||
implementation(libs.androidx.biometric.ktx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.privacylock
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.app_name
|
||||
import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed
|
||||
import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.biometric_authentication_failed_explainer_with_error
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/** The action waiting on the keyguard activity, which only its result may run or replace. */
|
||||
private class PendingApproval {
|
||||
var action: (() -> Unit)? = null
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberDeviceAuthenticator(): DeviceAuthenticator {
|
||||
val context = LocalContext.current
|
||||
val labels = rememberAuthPromptLabels()
|
||||
val pending = remember { PendingApproval() }
|
||||
|
||||
val keyguardLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
val action = pending.action
|
||||
pending.action = null
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
action?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
return remember(context, labels, keyguardLauncher) {
|
||||
DeviceAuthenticator { title, onApproved, onError ->
|
||||
pending.action = onApproved
|
||||
authenticate(
|
||||
title = title,
|
||||
context = context,
|
||||
labels = labels,
|
||||
keyguardLauncher = keyguardLauncher,
|
||||
onApproved = {
|
||||
pending.action = null
|
||||
onApproved()
|
||||
},
|
||||
onError = onError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Context.findFragmentActivity(): FragmentActivity? {
|
||||
var current: Context = this
|
||||
while (current is ContextWrapper) {
|
||||
if (current is FragmentActivity) return current
|
||||
current = current.baseContext
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Labels the device-credential and biometric prompts need. Resolved in
|
||||
* composition and passed down because [authenticate] runs from an onClick
|
||||
* lambda and from Android's own biometric callbacks, neither of which can
|
||||
* reach a Compose resource.
|
||||
*/
|
||||
@Immutable
|
||||
private data class AuthPromptLabels(
|
||||
val appName: String,
|
||||
val failedTitle: String,
|
||||
val failedExplainer: String,
|
||||
val failedExplainerWithError: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun rememberAuthPromptLabels(): AuthPromptLabels =
|
||||
AuthPromptLabels(
|
||||
appName = stringRes(Res.string.app_name),
|
||||
failedTitle = stringRes(Res.string.biometric_authentication_failed),
|
||||
failedExplainer = stringRes(Res.string.biometric_authentication_failed_explainer),
|
||||
failedExplainerWithError = stringRes(Res.string.biometric_authentication_failed_explainer_with_error),
|
||||
)
|
||||
|
||||
private fun authenticate(
|
||||
title: String,
|
||||
context: Context,
|
||||
labels: AuthPromptLabels,
|
||||
keyguardLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>,
|
||||
onApproved: () -> Unit,
|
||||
onError: (String, String) -> Unit,
|
||||
) {
|
||||
val fragmentContext = context.findFragmentActivity() ?: return onApproved()
|
||||
val keyguardManager =
|
||||
fragmentContext.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
|
||||
if (!keyguardManager.isDeviceSecure) {
|
||||
onApproved()
|
||||
return
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun keyguardPrompt() {
|
||||
val intent =
|
||||
keyguardManager.createConfirmDeviceCredentialIntent(
|
||||
labels.appName,
|
||||
title,
|
||||
)
|
||||
|
||||
keyguardLauncher.launch(intent)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||
keyguardPrompt()
|
||||
return
|
||||
}
|
||||
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val authenticators =
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
|
||||
val promptInfo =
|
||||
BiometricPrompt.PromptInfo
|
||||
.Builder()
|
||||
.setTitle(labels.appName)
|
||||
.setSubtitle(title)
|
||||
.setAllowedAuthenticators(authenticators)
|
||||
.build()
|
||||
|
||||
val biometricPrompt =
|
||||
BiometricPrompt(
|
||||
fragmentContext,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(
|
||||
errorCode: Int,
|
||||
errString: CharSequence,
|
||||
) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
|
||||
keyguardPrompt()
|
||||
}
|
||||
|
||||
BiometricPrompt.ERROR_LOCKOUT -> {
|
||||
keyguardPrompt()
|
||||
}
|
||||
|
||||
else -> {
|
||||
onError(
|
||||
labels.failedTitle,
|
||||
labels.failedExplainerWithError.format(errString.toString()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
onError(labels.failedTitle, labels.failedExplainer)
|
||||
}
|
||||
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
onApproved()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
when (biometricManager.canAuthenticate(authenticators)) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> biometricPrompt.authenticate(promptInfo)
|
||||
else -> keyguardPrompt()
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.privacylock
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/** Asks the user to prove it is them (biometrics or the device PIN) before a sensitive action. */
|
||||
fun interface DeviceAuthenticator {
|
||||
/**
|
||||
* Prompts with [title] and runs [onApproved] once the user passes. A device without a screen
|
||||
* lock approves at once. [onError] gets a title and a message for a failed attempt.
|
||||
*/
|
||||
fun authenticate(
|
||||
title: String,
|
||||
onApproved: () -> Unit,
|
||||
onError: (title: String, message: String) -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The platform's device-credential prompt. Android shows BiometricPrompt (strong biometrics or
|
||||
* the device credential) and falls back to the keyguard screen; desktop and iOS have no prompt
|
||||
* wired yet and approve at once, as Android does on a device without a screen lock.
|
||||
*/
|
||||
@Composable
|
||||
expect fun rememberDeviceAuthenticator(): DeviceAuthenticator
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.privacylock
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val ApproveAtOnce = DeviceAuthenticator { _, onApproved, _ -> onApproved() }
|
||||
|
||||
/** No device-credential prompt wired on this platform yet; see the common declaration. */
|
||||
@Composable
|
||||
actual fun rememberDeviceAuthenticator(): DeviceAuthenticator = ApproveAtOnce
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.privacylock
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val ApproveAtOnce = DeviceAuthenticator { _, onApproved, _ -> onApproved() }
|
||||
|
||||
/** No device-credential prompt wired on this platform yet; see the common declaration. */
|
||||
@Composable
|
||||
actual fun rememberDeviceAuthenticator(): DeviceAuthenticator = ApproveAtOnce
|
||||
Reference in New Issue
Block a user