From 0c81e17964228e3fa042412b2ebed66f7f9b80e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 8 Mar 2026 00:24:53 +0000 Subject: [PATCH 1/5] Add per-relay permissions for kind 22242 client authentication (NIP-42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kind 22242 events contain a ["relay", "wss://..."] tag identifying which relay is requesting authentication. This change makes permissions for these events per-relay URL rather than global: - DB: Add `relay` field to ApplicationPermissionsEntity (migration 16→17) with a new unique index on (pkKey, type, kind, relay). Non-22242 permissions keep relay="" to maintain backward compatibility. - DAO: Add getPermissionForRelay() and getWildcardRelayPermission() for relay-specific lookups, plus deletePermissions(key, type, kind, relay) and deletePermissionsForKind() helpers. - AmberUtils: acceptOrRejectPermission() and acceptPermission() accept an optional relay parameter. Wildcard "*" clears all relay-specific entries for a kind; a specific URL only clears its own entry. - UI: New BunkerRelayAuthScreen composable shows the relay URL prominently and presents a 2-option "Authentication scope" toggle: · "This relay only" — stores permission for the specific relay URL · "All relays" — stores permission with wildcard "*" Both options still include the RememberMyChoice time-based memory control. - BunkerSingleEventHomeScreen & IntentSingleEventHomeScreen: detect kind 22242 requests and show BunkerRelayAuthScreen instead of the generic BunkerEventData. Permission lookup checks specific relay first, then the "*" wildcard fallback. - SignerProvider (ContentProvider path): kind 22242 checks relay-specific permission first, then wildcard, before auto-approving. - AmberToggles: add optional segmentWidth parameter (default 55.dp) so the relay scope toggle can use wider 120dp segments. https://claude.ai/code/session_01Dm9zoUbbzdJzuXTNdAVhfW --- .../greenart7c3/nostrsigner/SignerProvider.kt | 11 +- .../nostrsigner/database/AppDatabase.kt | 11 +- .../nostrsigner/database/ApplicationDao.kt | 48 +++++- .../database/ApplicationPermissionsEntity.kt | 3 +- .../nostrsigner/service/AmberUtils.kt | 26 ++- .../nostrsigner/service/BunkerRequestUtils.kt | 4 + .../nostrsigner/service/IntentUtils.kt | 4 + .../nostrsigner/ui/components/AmberToggles.kt | 4 +- .../ui/components/BunkerRelayAuthScreen.kt | 149 ++++++++++++++++++ .../components/BunkerSingleEventHomeScreen.kt | 77 +++++++++ .../components/IntentSingleEventHomeScreen.kt | 70 ++++++++ app/src/main/res/values/strings.xml | 5 + 12 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt index 3c6eba50..a74de970 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/SignerProvider.kt @@ -12,6 +12,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.IntentUtils +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.Hex @@ -175,7 +176,12 @@ class SignerProvider : ContentProvider() { val database = Amber.instance.getDatabase(account.npub) val historyDatabase = Amber.instance.getHistoryDatabase(account.npub) - var permission = + var permission = if (event.kind == 22242) { + // Kind 22242 = relay client auth (NIP-42): check relay-specific permission first + val relayUrl = AmberEvent.relay(event) ?: "" + database.dao().getPermissionForRelay(packageName, "SIGN_EVENT", 22242, relayUrl) + ?: database.dao().getWildcardRelayPermission(packageName, "SIGN_EVENT", 22242) + } else { database .dao() .getPermission( @@ -183,7 +189,8 @@ class SignerProvider : ContentProvider() { "SIGN_EVENT", event.kind, ) - if (permission == null) { + } + if (permission == null && event.kind != 22242) { event.kind.kindToNip()?.let { val nipNumber = it.toIntOrNull() permission = if (nipNumber == null) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt index f1e9e7f8..eeacb1e0 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/AppDatabase.kt @@ -148,12 +148,20 @@ val MIGRATION_15_16 = object : Migration(15, 16) { } } +val MIGRATION_16_17 = object : Migration(16, 17) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE `applicationPermission` ADD COLUMN `relay` TEXT NOT NULL DEFAULT ''") + db.execSQL("DROP INDEX IF EXISTS `permissions_unique`") + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `permissions_unique` ON `applicationPermission` (`pkKey`, `type`, `kind`, `relay`)") + } +} + @Database( entities = [ ApplicationEntity::class, ApplicationPermissionsEntity::class, ], - version = 16, + version = 17, ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -190,6 +198,7 @@ abstract class AppDatabase : RoomDatabase() { .addMigrations(MIGRATION_13_14) .addMigrations(MIGRATION_14_15) .addMigrations(MIGRATION_15_16) + .addMigrations(MIGRATION_16_17) .build() instance diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt index 5df4295e..d776994b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationDao.kt @@ -59,7 +59,7 @@ interface ApplicationDao { @Transaction fun getAllAcceptedPermissions(): List - @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind") + @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = ''") fun getPermission( key: String, type: String, @@ -72,6 +72,21 @@ interface ApplicationDao { type: String, ): ApplicationPermissionsEntity? + @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = :relay LIMIT 1") + fun getPermissionForRelay( + key: String, + type: String, + kind: Int, + relay: String, + ): ApplicationPermissionsEntity? + + @Query("SELECT * FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = '*' LIMIT 1") + fun getWildcardRelayPermission( + key: String, + type: String, + kind: Int, + ): ApplicationPermissionsEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) @Transaction suspend fun insertApplication(event: ApplicationEntity): Long? @@ -84,7 +99,17 @@ interface ApplicationDao { suspend fun insertPermissions(permissions: List): List? { permissions.forEach { if (it.kind != null) { - deletePermissions(it.pkKey, it.type, it.kind) + if (it.relay.isNotEmpty()) { + // For relay-specific permissions (kind 22242): wildcard "*" clears all relay entries, + // specific relay only clears its own entry + if (it.relay == "*") { + deletePermissionsForKind(it.pkKey, it.type, it.kind) + } else { + deletePermissions(it.pkKey, it.type, it.kind, it.relay) + } + } else { + deletePermissions(it.pkKey, it.type, it.kind) + } } else { deletePermissions(it.pkKey, it.type) } @@ -107,7 +132,7 @@ interface ApplicationDao { type: String, ) - @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type and kind = :kind") + @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = ''") @Transaction suspend fun deletePermissions( key: String, @@ -115,6 +140,23 @@ interface ApplicationDao { kind: Int, ) + @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind AND relay = :relay") + @Transaction + suspend fun deletePermissions( + key: String, + type: String, + kind: Int, + relay: String, + ) + + @Query("DELETE FROM applicationPermission WHERE pkKey = :key AND type = :type AND kind = :kind") + @Transaction + suspend fun deletePermissionsForKind( + key: String, + type: String, + kind: Int, + ) + @Insert(onConflict = OnConflictStrategy.IGNORE) @Transaction suspend fun insertApplicationWithPermissions(application: ApplicationWithPermissions) { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt index 73fd7292..7228c7c6 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/database/ApplicationPermissionsEntity.kt @@ -21,7 +21,7 @@ import androidx.room.PrimaryKey name = "permissions_by_pk_key", ), Index( - value = ["pkKey", "type", "kind"], + value = ["pkKey", "type", "kind", "relay"], name = "permissions_unique", unique = true, ), @@ -37,4 +37,5 @@ data class ApplicationPermissionsEntity( val rememberType: Int, var acceptUntil: Long, var rejectUntil: Long, + val relay: String = "", ) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt index c4596200..7e28e66f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/AmberUtils.kt @@ -81,6 +81,7 @@ object AmberUtils { value: Boolean, rememberType: RememberType, account: Account, + relay: String = "", ) { val until = when (rememberType) { RememberType.ALWAYS -> Long.MAX_VALUE / 1000 @@ -91,7 +92,17 @@ object AmberUtils { } if (kind != null) { - application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() } + if (relay.isNotEmpty()) { + // For relay-specific permissions: wildcard "*" removes all relay entries for this kind, + // specific relay removes only its own entry + if (relay == "*") { + application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() } + } else { + application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() && it.relay == relay } + } + } else { + application.permissions.removeIf { it.kind == kind && it.type == signerType.toString() && it.relay.isEmpty() } + } } else { application.permissions.removeIf { it.type == signerType.toString() && it.type != "SIGN_EVENT" } } @@ -106,6 +117,7 @@ object AmberUtils { rememberType.screenCode, if (value) until else 0L, if (!value) until else 0L, + relay, ), ) @@ -173,6 +185,7 @@ object AmberUtils { type: SignerType, kind: Int?, rememberType: RememberType, + relay: String = "", ) { val until = when (rememberType) { RememberType.ALWAYS -> Long.MAX_VALUE / 1000 @@ -183,7 +196,15 @@ object AmberUtils { } if (kind != null) { - application.permissions.removeIf { it.kind == kind && it.type == type.toString() } + if (relay.isNotEmpty()) { + if (relay == "*") { + application.permissions.removeIf { it.kind == kind && it.type == type.toString() } + } else { + application.permissions.removeIf { it.kind == kind && it.type == type.toString() && it.relay == relay } + } + } else { + application.permissions.removeIf { it.kind == kind && it.type == type.toString() && it.relay.isEmpty() } + } } else { application.permissions.removeIf { it.type == type.toString() && it.type != "SIGN_EVENT" } } @@ -198,6 +219,7 @@ object AmberUtils { rememberType.screenCode, until, 0, + relay, ), ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt index 243849b4..e64f5f2b 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/BunkerRequestUtils.kt @@ -240,6 +240,7 @@ object BunkerRequestUtils { rememberType: RememberType, oldKey: String = "", deleteAfter: Long = 0L, + relay: String = "", ) { onLoading(true) Amber.instance.applicationIOScope.launch { @@ -319,6 +320,7 @@ object BunkerRequestUtils { type = type, kind = kind, rememberType = rememberType, + relay = relay, ) } @@ -434,6 +436,7 @@ object BunkerRequestUtils { signerType: SignerType, kind: Int?, onLoading: (Boolean) -> Unit, + relay: String = "", ) { onLoading(true) Amber.instance.applicationIOScope.launch(Dispatchers.IO) { @@ -481,6 +484,7 @@ object BunkerRequestUtils { false, rememberType, account, + relay, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt index cec35f36..a8444639 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/IntentUtils.kt @@ -681,6 +681,7 @@ object IntentUtils { shouldCloseApplication: Boolean? = null, rememberType: RememberType, deleteAfter: Long = 0L, + relay: String = "", ) { onLoading(true) Amber.instance.applicationIOScope.launch { @@ -732,6 +733,7 @@ object IntentUtils { type = intentData.type, kind = kind, rememberType = rememberType, + relay = relay, ) } @@ -864,6 +866,7 @@ object IntentUtils { kind: Int?, onLoading: (Boolean) -> Unit, onRemoveIntentData: (List, IntentResultType) -> Unit, + relay: String = "", ) { Amber.instance.applicationIOScope.launch(Dispatchers.IO) { if (key == "null") { @@ -901,6 +904,7 @@ object IntentUtils { false, rememberType, account, + relay, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt index 7be4bc72..2d6aac92 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberToggles.kt @@ -20,15 +20,17 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @Composable fun AmberToggles( selectedIndex: Int, count: Int, + segmentWidth: Dp = 55.dp, content: @Composable RowScope.() -> Unit, ) { - val fixedSegmentWidth = 55.dp + val fixedSegmentWidth = segmentWidth val padding = 2.dp val totalWidth = (fixedSegmentWidth * count) + (padding * 2) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt new file mode 100644 index 00000000..d4c769a5 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt @@ -0,0 +1,149 @@ +package com.greenart7c3.nostrsigner.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.Account +import com.greenart7c3.nostrsigner.ui.RememberType + +/** + * Scope for a kind 22242 relay authentication permission. + * [SPECIFIC] means the permission applies only to the specific relay URL in the event. + * [ALL] means the permission applies to all relays (wildcard "*"). + */ +enum class RelayAuthScope { + SPECIFIC, + ALL, +} + +@Composable +fun BunkerRelayAuthScreen( + modifier: Modifier, + appName: String, + relayUrl: String, + shouldAcceptOrReject: Boolean?, + defaultScope: RelayAuthScope, + account: Account, + onAccept: (RememberType, RelayAuthScope) -> Unit, + onReject: (RememberType, RelayAuthScope) -> Unit, +) { + var rememberType by remember { mutableStateOf(RememberType.NEVER) } + var scope by remember { mutableStateOf(defaultScope) } + val scopeIndex by remember(scope) { mutableIntStateOf(if (scope == RelayAuthScope.SPECIFIC) 0 else 1) } + + if (shouldAcceptOrReject != null) { + LaunchedEffect(Unit) { + if (shouldAcceptOrReject) { + onAccept(RememberType.entries[0], scope) + } else { + onReject(RememberType.entries[0], scope) + } + } + } + + Column( + modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.size(16.dp)) + + Text( + buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(appName) + } + append(" ") + append(stringResource(R.string.relay_auth_request, "")) + }, + fontSize = 18.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(Modifier.size(8.dp)) + + Text( + relayUrl, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + + Spacer(Modifier.size(16.dp)) + + SigningAs(account) + + Spacer(modifier = Modifier.weight(1f)) + + LabeledBorderBox( + label = stringResource(R.string.relay_auth_scope), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + AmberToggles( + selectedIndex = scopeIndex, + count = 2, + segmentWidth = 120.dp, + content = { + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_this_relay_only), + isSelected = scope == RelayAuthScope.SPECIFIC, + onClick = { scope = RelayAuthScope.SPECIFIC }, + ) + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_all_relays), + isSelected = scope == RelayAuthScope.ALL, + onClick = { scope = RelayAuthScope.ALL }, + ) + }, + ) + } + + Spacer(Modifier.size(8.dp)) + + RememberMyChoice( + shouldRunAcceptOrReject = null, + packageName = null, + alwaysShow = true, + onAccept = { onAccept(it, scope) }, + onReject = { onReject(it, scope) }, + ) { + rememberType = it + } + + AcceptRejectButtons( + onAccept = { onAccept(rememberType, scope) }, + onReject = { onReject(rememberType, scope) }, + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt index 01cbb92b..40e8c13f 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerSingleEventHomeScreen.kt @@ -35,6 +35,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.service.BunkerRequestUtils import com.greenart7c3.nostrsigner.service.isPrivateEvent +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -616,6 +617,82 @@ fun BunkerSingleEventHomeScreen( Spacer(Modifier.size(8.dp)) Text("Not logged in") } + } else if (event.kind == 22242) { + // Kind 22242 = relay client authentication (NIP-42) + // Permission is per-relay URL extracted from the event's "relay" tag + val relayUrl = AmberEvent.relay(event) ?: "" + + // Check for a relay-specific permission first, then wildcard "*" (all relays) + val permission = applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == type.toString() && it.kind == 22242 && it.relay == relayUrl + } ?: applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == type.toString() && it.kind == 22242 && it.relay == "*" + } + + val acceptUntil = permission?.acceptUntil ?: 0 + val rejectUntil = permission?.rejectUntil ?: 0 + + val acceptOrReject = if (rejectUntil == 0L && acceptUntil == 0L) { + null + } else if (rejectUntil > TimeUtils.now() && rejectUntil > 0 && permission?.acceptable == false) { + false + } else if (acceptUntil > TimeUtils.now() && acceptUntil > 0 && permission?.acceptable == true) { + true + } else { + null + } + + BunkerRelayAuthScreen( + modifier = modifier, + appName = appName, + relayUrl = relayUrl, + shouldAcceptOrReject = acceptOrReject, + defaultScope = RelayAuthScope.SPECIFIC, + account = account, + onAccept = { rememberType, scope -> + if (event.pubKey != account.hexKey && !isPrivateEvent(event.kind, event.tags)) { + coroutineScope.launch { + Toast.makeText( + context, + context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user), + Toast.LENGTH_SHORT, + ).show() + } + return@BunkerRelayAuthScreen + } + + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + BunkerRequestUtils.sendResult( + context = context, + account = account, + key = key, + response = event.toJson(), + bunkerRequest = bunkerRequest, + kind = event.kind, + onLoading = onLoading, + permissions = null, + appName = appName, + signPolicy = null, + shouldCloseApplication = bunkerRequest.closeApplication, + rememberType = rememberType, + relay = relayPermission, + ) + }, + onReject = { rememberType, scope -> + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + BunkerRequestUtils.sendRejection( + key = key, + account = account, + bunkerRequest = bunkerRequest, + appName = appName, + rememberType = rememberType, + signerType = type, + kind = event.kind, + onLoading = onLoading, + relay = relayPermission, + ) + }, + ) } else { val permission = applicationEntity?.permissions?.firstOrNull { diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt index 17e4187b..164eedbe 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentSingleEventHomeScreen.kt @@ -30,6 +30,7 @@ import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.kindToNip import com.greenart7c3.nostrsigner.service.IntentUtils import com.greenart7c3.nostrsigner.service.isPrivateEvent +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.ToastManager import com.vitorpamplona.quartz.nip01Core.core.toHexKey @@ -242,6 +243,75 @@ fun IntentSingleEventHomeScreen( Spacer(Modifier.size(8.dp)) Text("Not logged in") } + } else if (event.kind == 22242) { + // Kind 22242 = relay client authentication (NIP-42) + // Permission is per-relay URL extracted from the event's "relay" tag + val relayUrl = AmberEvent.relay(event) ?: "" + + // Check for relay-specific permission first, then wildcard "*" (all relays) + val permission = applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == intentData.type.toString() && it.kind == 22242 && it.relay == relayUrl + } ?: applicationEntity?.permissions?.firstOrNull { + it.pkKey == key && it.type == intentData.type.toString() && it.kind == 22242 && it.relay == "*" + } + + val acceptOrReject = if (permission == null) { + IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, null) + } else { + IntentUtils.isRemembered(applicationEntity?.application?.signPolicy, permission) + } + + BunkerRelayAuthScreen( + modifier = modifier, + appName = appName, + relayUrl = relayUrl, + shouldAcceptOrReject = acceptOrReject, + defaultScope = RelayAuthScope.SPECIFIC, + account = account, + onAccept = { rememberType, scope -> + if (intentData.unsignedEventKey.isNotBlank() && intentData.unsignedEventKey != account.hexKey && !isPrivateEvent(event.kind, event.tags)) { + ToastManager.toast( + title = context.getString(R.string.warning), + message = context.getString(R.string.event_pubkey_is_not_equal_to_current_logged_in_user), + ) + return@BunkerRelayAuthScreen + } + + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + IntentUtils.sendResult( + context = context, + packageName = packageName, + account = account, + key = key, + clipboardManager = clipboardManager, + event = event.toJson(), + value = event.sig, + intentData = intentData, + kind = event.kind, + onLoading = onLoading, + onRemoveIntentData = onRemoveIntentData, + signPolicy = null, + appName = applicationName ?: appName, + permissions = null, + rememberType = rememberType, + relay = relayPermission, + ) + }, + onReject = { rememberType, scope -> + val relayPermission = if (scope == RelayAuthScope.ALL) "*" else relayUrl + IntentUtils.sendRejection( + key = key, + account = account, + intentData = intentData, + appName = appName, + rememberType = rememberType, + onLoading = onLoading, + onRemoveIntentData = onRemoveIntentData, + kind = event.kind, + relay = relayPermission, + ) + }, + ) } else { val permission = applicationEntity?.permissions?.firstOrNull { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdedc7fe..40cac538 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -643,4 +643,9 @@ Account picture Encrypt and copy to clipboard Encrypt and show QR Code + Relay authentication + %1$s wants to authenticate to relay + This relay only + All relays + Authentication scope From 5b2d4cd3fbfa1f9479eeae494c4e4d2e2c5e4c94 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Mar 2026 10:48:38 +0000 Subject: [PATCH 2/5] Extend per-relay auth permissions to multi-event screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kind 22242 relay auth events in the batch approve/discard flows now store relay-specific permissions just like the single-event screens: - BunkerMultiEventHomeScreen: discard-all extracts kind + relay for SIGN_EVENT requests; approve-all passes relay URL for kind 22242 sign requests to acceptOrRejectPermission. - IntentMultiEventHomeScreen: same treatment for the Intent path — discard-all passes kind + relay for SIGN_EVENT; approve-all passes the relay tag from the event for kind 22242. Non-22242 events are unaffected (relay stays ""). https://claude.ai/code/session_01Dm9zoUbbzdJzuXTNdAVhfW --- .../ui/components/BunkerMultiEventHomeScreen.kt | 16 +++++++++++++++- .../ui/components/IntentMultiEventHomeScreen.kt | 16 +++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 08f075f2..31bbbb64 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -38,6 +38,7 @@ import com.greenart7c3.nostrsigner.service.ApplicationNameCache import com.greenart7c3.nostrsigner.service.BunkerRequestUtils import com.greenart7c3.nostrsigner.service.EventNotificationConsumer import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.navigation.Route @@ -294,14 +295,21 @@ fun BunkerMultiEventHomeScreen( ) if (request.rememberType.value != RememberType.NEVER && request.checked.value) { + val rejectKind = if (request.request is BunkerRequestSign) request.request.event.kind else null + val rejectRelay = if (request.request is BunkerRequestSign && request.request.event.kind == 22242) { + AmberEvent.relay(request.request.event) ?: "" + } else { + "" + } AmberUtils.acceptOrRejectPermission( application, localKey, BunkerRequestUtils.getTypeFromBunker(request.request), - null, + rejectKind, false, request.rememberType.value, thisAccount, + relay = rejectRelay, ) } @@ -376,6 +384,11 @@ fun BunkerMultiEventHomeScreen( val localEvent = request.signedEvent!! if (request.rememberType.value != RememberType.NEVER && request.checked.value) { + val signRelay = if (localEvent.kind == 22242) { + AmberEvent.relay(localEvent) ?: "" + } else { + "" + } AmberUtils.acceptOrRejectPermission( application = application, key = localKey, @@ -384,6 +397,7 @@ fun BunkerMultiEventHomeScreen( value = true, rememberType = request.rememberType.value, account = thisAccount, + relay = signRelay, ) } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 7cbd77bb..ea5bcced 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -41,6 +41,7 @@ import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind import com.greenart7c3.nostrsigner.service.AmberUtils import com.greenart7c3.nostrsigner.service.ApplicationNameCache import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents +import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType import com.greenart7c3.nostrsigner.ui.navigation.Route @@ -298,14 +299,21 @@ fun IntentMultiEventHomeScreen( ) if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { + val rejectKind = if (intentData.type == SignerType.SIGN_EVENT) intentData.event?.kind else null + val rejectRelay = if (intentData.type == SignerType.SIGN_EVENT && intentData.event?.kind == 22242) { + intentData.event?.let { AmberEvent.relay(it) } ?: "" + } else { + "" + } AmberUtils.acceptOrRejectPermission( application, localKey, intentData.type, - null, + rejectKind, false, intentData.rememberType.value, thisAccount, + relay = rejectRelay, ) } @@ -388,6 +396,11 @@ fun IntentMultiEventHomeScreen( val localEvent = intentData.event!! if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { + val signRelay = if (localEvent.kind == 22242) { + AmberEvent.relay(localEvent) ?: "" + } else { + "" + } AmberUtils.acceptOrRejectPermission( application, localKey, @@ -396,6 +409,7 @@ fun IntentMultiEventHomeScreen( true, intentData.rememberType.value, thisAccount, + relay = signRelay, ) } From b44bfacf461c5b3ca2d1e1dffa8cfd6c3fb89135 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 9 Mar 2026 07:56:17 -0300 Subject: [PATCH 3/5] Add database schema version 17 and extend AmberEvent relay helper - Add Room database schema for version 17, defining tables for `application` and `applicationPermission` with associated indices and foreign keys. - Add an overloaded `AmberEvent.relay()` helper method to support extracting relay information from `EventTemplate`. - Add a missing `withStyle` import in `BunkerRelayAuthScreen.kt`. --- .../17.json | 229 ++++++++++++++++++ .../nostrsigner/service/model/AmberEvent.kt | 3 + .../ui/components/BunkerRelayAuthScreen.kt | 1 + 3 files changed, 233 insertions(+) create mode 100644 app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/17.json diff --git a/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/17.json b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/17.json new file mode 100644 index 00000000..7e17641a --- /dev/null +++ b/app/schemas/com.greenart7c3.nostrsigner.database.AppDatabase/17.json @@ -0,0 +1,229 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "cc242d2c391a6dec67e7613458bffd30", + "entities": [ + { + "tableName": "application", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `name` TEXT NOT NULL, `relays` TEXT NOT NULL, `url` TEXT NOT NULL, `icon` TEXT NOT NULL, `description` TEXT NOT NULL, `pubKey` TEXT NOT NULL, `isConnected` INTEGER NOT NULL, `secret` TEXT NOT NULL, `useSecret` INTEGER NOT NULL, `signPolicy` INTEGER NOT NULL, `closeApplication` INTEGER NOT NULL, `deleteAfter` INTEGER NOT NULL, `lastUsed` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "relays", + "columnName": "relays", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubKey", + "columnName": "pubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isConnected", + "columnName": "isConnected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "secret", + "columnName": "secret", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "useSecret", + "columnName": "useSecret", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "signPolicy", + "columnName": "signPolicy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "closeApplication", + "columnName": "closeApplication", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "deleteAfter", + "columnName": "deleteAfter", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUsed", + "columnName": "lastUsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [ + { + "name": "index_key", + "unique": true, + "columnNames": [ + "key" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_key` ON `${TABLE_NAME}` (`key`)" + }, + { + "name": "index_name", + "unique": false, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_name` ON `${TABLE_NAME}` (`name`)" + } + ] + }, + { + "tableName": "applicationPermission", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `pkKey` TEXT NOT NULL, `type` TEXT NOT NULL, `kind` INTEGER, `acceptable` INTEGER NOT NULL, `rememberType` INTEGER NOT NULL, `acceptUntil` INTEGER NOT NULL, `rejectUntil` INTEGER NOT NULL, `relay` TEXT NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`pkKey`) REFERENCES `application`(`key`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER" + }, + { + "fieldPath": "pkKey", + "columnName": "pkKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "INTEGER" + }, + { + "fieldPath": "acceptable", + "columnName": "acceptable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rememberType", + "columnName": "rememberType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "acceptUntil", + "columnName": "acceptUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rejectUntil", + "columnName": "rejectUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "relay", + "columnName": "relay", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "permissions_by_pk_key", + "unique": false, + "columnNames": [ + "pkKey" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `permissions_by_pk_key` ON `${TABLE_NAME}` (`pkKey`)" + }, + { + "name": "permissions_unique", + "unique": true, + "columnNames": [ + "pkKey", + "type", + "kind", + "relay" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `permissions_unique` ON `${TABLE_NAME}` (`pkKey`, `type`, `kind`, `relay`)" + } + ], + "foreignKeys": [ + { + "table": "application", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pkKey" + ], + "referencedColumns": [ + "key" + ] + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cc242d2c391a6dec67e7613458bffd30')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt index a5d25db1..390e6321 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/model/AmberEvent.kt @@ -17,6 +17,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.jackson.toTypedArray +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip02FollowList.tags.ContactTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -102,5 +103,7 @@ open class AmberEvent( fun fromJson(json: String): AmberEvent = mapper.readValue(json, AmberEvent::class.java) fun relay(event: Event): String? = event.tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.firstOrNull() + + fun relay(event: EventTemplate): String? = event.tags.filter { it.size > 1 && it[0] == "relay" }.map { it[1] }.firstOrNull() } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt index d4c769a5..9b39072c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerRelayAuthScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.greenart7c3.nostrsigner.R From 34da50029c6dc9adf5fb3a6d05d8b42b372671cd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Mar 2026 11:03:33 +0000 Subject: [PATCH 4/5] Show relay in edit-permission screen; add relay scope to multi-event screens EditPermission / PermissionRow: - Kind 22242 permissions now show their relay below the main label. Wildcard "*" displays "All relays"; a specific URL is shown as-is. BunkerMultiEventHomeScreen / IntentMultiEventHomeScreen: - When a batch contains kind 22242 relay-auth events an "Authentication scope" toggle appears (same as the single-event BunkerRelayAuthScreen): "This relay only" vs "All relays". - The selected scope is applied to every kind 22242 event in both the approve-all and discard-all flows. https://claude.ai/code/session_01Dm9zoUbbzdJzuXTNdAVhfW --- .../nostrsigner/ui/EditPermission.kt | 18 +++++++++- .../components/BunkerMultiEventHomeScreen.kt | 35 +++++++++++++++++-- .../components/IntentMultiEventHomeScreen.kt | 35 +++++++++++++++++-- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt index ea743dcb..dace6c08 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt @@ -294,7 +294,7 @@ fun PermissionRow( onDelete: (ApplicationPermissionsEntity) -> Unit, ) { val context = LocalContext.current - val message = remember(permission.type, permission.kind, permission.acceptable) { + val message = remember(permission.type, permission.kind, permission.acceptable, permission.relay) { val localPermission = Permission( permission.type.toLowerCase(Locale.current), permission.kind, @@ -344,6 +344,22 @@ fun PermissionRow( overflow = TextOverflow.Ellipsis, ) + if (permission.kind == 22242 && permission.relay.isNotEmpty()) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, bottom = 8.dp), + text = if (permission.relay == "*") { + context.getString(R.string.for_all_relays) + } else { + permission.relay + }, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + AmberToggles( count = 3, selectedIndex = optionIndex, diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 31bbbb64..ee4a79f7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults @@ -104,6 +105,8 @@ fun BunkerMultiEventHomeScreen( var localAccount by remember { mutableStateOf("") } val key = bunkerRequests.first().localKey var rememberType by remember { mutableStateOf(RememberType.NEVER) } + var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } + val hasRelayAuthEvents = groupedEvents.keys.contains(22242) var appName by remember { mutableStateOf(ApplicationNameCache.names["$localAccount-$key"] ?: key.toShortenHex()) } LaunchedEffect(Unit) { @@ -224,6 +227,34 @@ fun BunkerMultiEventHomeScreen( } } + if (hasRelayAuthEvents) { + LabeledBorderBox( + label = stringResource(R.string.relay_auth_scope), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 8.dp), + ) { + AmberToggles( + selectedIndex = if (relayAuthScope == RelayAuthScope.SPECIFIC) 0 else 1, + count = 2, + segmentWidth = 120.dp, + ) { + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_this_relay_only), + isSelected = relayAuthScope == RelayAuthScope.SPECIFIC, + onClick = { relayAuthScope = RelayAuthScope.SPECIFIC }, + ) + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_all_relays), + isSelected = relayAuthScope == RelayAuthScope.ALL, + onClick = { relayAuthScope = RelayAuthScope.ALL }, + ) + } + } + } + RememberMyChoice( alwaysShow = true, shouldRunAcceptOrReject = null, @@ -297,7 +328,7 @@ fun BunkerMultiEventHomeScreen( if (request.rememberType.value != RememberType.NEVER && request.checked.value) { val rejectKind = if (request.request is BunkerRequestSign) request.request.event.kind else null val rejectRelay = if (request.request is BunkerRequestSign && request.request.event.kind == 22242) { - AmberEvent.relay(request.request.event) ?: "" + if (relayAuthScope == RelayAuthScope.ALL) "*" else (AmberEvent.relay(request.request.event) ?: "") } else { "" } @@ -385,7 +416,7 @@ fun BunkerMultiEventHomeScreen( if (request.rememberType.value != RememberType.NEVER && request.checked.value) { val signRelay = if (localEvent.kind == 22242) { - AmberEvent.relay(localEvent) ?: "" + if (relayAuthScope == RelayAuthScope.ALL) "*" else (AmberEvent.relay(localEvent) ?: "") } else { "" } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index ea5bcced..a1ef1671 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ButtonDefaults @@ -106,6 +107,8 @@ fun IntentMultiEventHomeScreen( var localAccount by remember { mutableStateOf("") } val key = "$packageName" var rememberType by remember { mutableStateOf(RememberType.NEVER) } + var relayAuthScope by remember { mutableStateOf(RelayAuthScope.SPECIFIC) } + val hasRelayAuthEvents = groupedEvents.keys.contains(22242) LaunchedEffect(Unit) { launch(Dispatchers.IO) { @@ -233,6 +236,34 @@ fun IntentMultiEventHomeScreen( } } + if (hasRelayAuthEvents) { + LabeledBorderBox( + label = stringResource(R.string.relay_auth_scope), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 8.dp), + ) { + AmberToggles( + selectedIndex = if (relayAuthScope == RelayAuthScope.SPECIFIC) 0 else 1, + count = 2, + segmentWidth = 120.dp, + ) { + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_this_relay_only), + isSelected = relayAuthScope == RelayAuthScope.SPECIFIC, + onClick = { relayAuthScope = RelayAuthScope.SPECIFIC }, + ) + ToggleOption( + modifier = Modifier.width(120.dp), + text = stringResource(R.string.for_all_relays), + isSelected = relayAuthScope == RelayAuthScope.ALL, + onClick = { relayAuthScope = RelayAuthScope.ALL }, + ) + } + } + } + RememberMyChoice( alwaysShow = true, shouldRunAcceptOrReject = null, @@ -301,7 +332,7 @@ fun IntentMultiEventHomeScreen( if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { val rejectKind = if (intentData.type == SignerType.SIGN_EVENT) intentData.event?.kind else null val rejectRelay = if (intentData.type == SignerType.SIGN_EVENT && intentData.event?.kind == 22242) { - intentData.event?.let { AmberEvent.relay(it) } ?: "" + if (relayAuthScope == RelayAuthScope.ALL) "*" else (intentData.event?.let { AmberEvent.relay(it) } ?: "") } else { "" } @@ -397,7 +428,7 @@ fun IntentMultiEventHomeScreen( if (intentData.rememberType.value != RememberType.NEVER && intentData.checked.value) { val signRelay = if (localEvent.kind == 22242) { - AmberEvent.relay(localEvent) ?: "" + if (relayAuthScope == RelayAuthScope.ALL) "*" else (AmberEvent.relay(localEvent) ?: "") } else { "" } From 10d8ce2c346e136a325d3e10f78b85d5f6b92ca7 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 9 Mar 2026 08:19:05 -0300 Subject: [PATCH 5/5] Reduce bottom padding in EditPermission relay text - Remove `bottom = 8.dp` padding from the relay description `Text` component to improve layout consistency. --- .../main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt index dace6c08..6473c70c 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/EditPermission.kt @@ -348,7 +348,7 @@ fun PermissionRow( Text( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, bottom = 8.dp), + .padding(horizontal = 16.dp), text = if (permission.relay == "*") { context.getString(R.string.for_all_relays) } else {