From 6391ddbbaad8aac3eb68f9e083c0c5846d0519eb Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 24 Sep 2025 09:09:51 -0300 Subject: [PATCH] Show a dialog to select a signer when using multiple signers --- .../loggedOff/login/ExternalSignerButton.kt | 112 +++++++++++++++++- amethyst/src/main/res/values/strings.xml | 1 + .../client/ExternalSignerLogin.kt | 8 +- .../client/IsExternalSignerInstalled.kt | 12 ++ 4 files changed, 131 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt index 4285dd087..6280c09bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/login/ExternalSignerButton.kt @@ -23,12 +23,39 @@ package com.vitorpamplona.amethyst.ui.screen.loggedOff.login import android.app.Activity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.core.graphics.drawable.toBitmap +import coil3.compose.rememberAsyncImagePainter import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.DefaultSignerPermissions import com.vitorpamplona.amethyst.ui.theme.Size0dp @@ -37,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.client.ExternalSignerLogin +import com.vitorpamplona.quartz.nip55AndroidSigner.client.getExternalSignersInstalled import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch @@ -44,6 +72,9 @@ import kotlinx.coroutines.launch @Composable fun ExternalSignerButton(loginViewModel: LoginViewModel) { val scope = rememberCoroutineScope() + val context = LocalContext.current + val installedSigners = getExternalSignersInstalled(context) + var shouldSelectSigner by remember { mutableStateOf(false) } val launcher = rememberLauncherForActivityResult( @@ -63,6 +94,81 @@ fun ExternalSignerButton(loginViewModel: LoginViewModel) { } } + if (shouldSelectSigner) { + Dialog( + onDismissRequest = { + shouldSelectSigner = false + }, + content = { + Surface( + shape = RoundedCornerShape(4.dp), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.padding(8.dp), + text = stringResource(R.string.select_signer), + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + ) + Spacer(Modifier.height(4.dp)) + LazyColumn { + items(installedSigners) { + val appName = it.loadLabel(context.packageManager).toString() + val appIcon = it.loadIcon(context.packageManager) + val iconBitmap = appIcon.toBitmap() + + Row( + Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp, top = 8.dp) + .clickable { + if (!loginViewModel.acceptedTerms) { + loginViewModel.termsAcceptanceIsRequiredError = true + } else { + try { + launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions, it.activityInfo.packageName)) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("ExternalSigner", "Error opening Signer app", e) + loginViewModel.errorManager.error(R.string.error_opening_external_signer) + } finally { + shouldSelectSigner = false + } + } + }, + ) { + val painter = + rememberAsyncImagePainter( + iconBitmap, + ) + + Image( + painter = painter, + contentDescription = appName, + modifier = + Modifier + .size(48.dp) + .padding(end = 16.dp), + ) + Column { + Text(appName) + Text( + it.activityInfo.packageName, + fontSize = 14.sp, + color = Color.Gray, + ) + } + } + } + } + } + } + }, + ) + } + Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) { LoginWithAmberButton( enabled = loginViewModel.acceptedTerms, @@ -71,7 +177,11 @@ fun ExternalSignerButton(loginViewModel: LoginViewModel) { loginViewModel.termsAcceptanceIsRequiredError = true } else { try { - launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions)) + if (installedSigners.size == 1) { + launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions)) + } else { + shouldSelectSigner = true + } } catch (e: Exception) { if (e is CancellationException) throw e Log.e("ExternalSigner", "Error opening Signer app", e) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d5b837e88..9c6f4283c 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1289,4 +1289,5 @@ Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared Send it This message will disappear in %1$d days + Select Signer diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt index 015c3fc65..2ec43bd09 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/ExternalSignerLogin.kt @@ -29,9 +29,15 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission object ExternalSignerLogin { - fun createIntent(permissions: List = LoginRequest.DefaultPermissions): Intent { + fun createIntent( + permissions: List = LoginRequest.DefaultPermissions, + packageName: String = "", + ): Intent { val intent = LoginRequest.assemble(permissions) intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + if (packageName.isNotBlank()) { + intent.`package` = packageName + } return intent } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt index 92d7f5e48..cef8fa631 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip55AndroidSigner/client/IsExternalSignerInstalled.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.client import android.annotation.SuppressLint import android.content.Context import android.content.Intent +import android.content.pm.ResolveInfo import androidx.core.net.toUri @SuppressLint("QueryPermissionsNeeded") @@ -35,3 +36,14 @@ fun isExternalSignerInstalled(context: Context): Boolean = }, 0, ).isNotEmpty() + +@SuppressLint("QueryPermissionsNeeded") +fun getExternalSignersInstalled(context: Context): List = + context.packageManager + .queryIntentActivities( + Intent().apply { + action = Intent.ACTION_VIEW + data = "nostrsigner:".toUri() + }, + 0, + )