Merge pull request #1493 from greenart7c3/main

Show a dialog to select a signer when using multiple signers
This commit is contained in:
Vitor Pamplona
2025-09-24 08:38:20 -04:00
committed by GitHub
4 changed files with 131 additions and 2 deletions

View File

@@ -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)

View File

@@ -1289,4 +1289,5 @@
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared</string>
<string name="crashreport_found_send">Send it</string>
<string name="this_message_will_disappear_in_days">This message will disappear in %1$d days</string>
<string name="select_signer">Select Signer</string>
</resources>

View File

@@ -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<Permission> = LoginRequest.DefaultPermissions): Intent {
fun createIntent(
permissions: List<Permission> = 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
}

View File

@@ -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<ResolveInfo> =
context.packageManager
.queryIntentActivities(
Intent().apply {
action = Intent.ACTION_VIEW
data = "nostrsigner:".toUri()
},
0,
)