app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 141e3ae1fa95978c705fde47ad59444d84d96a78
parent 647baa5e653a7d7bd4837963396f2ad1d31e0470
Author: triesap <tyson@radroots.org>
Date:   Sun,  2 Aug 2026 19:58:01 +0000

ui(accounts): show generated key backup receipt

- render generated npub and nsec only from transient receipt state
- provide explicit clipboard and acknowledgement actions
- warn that loss of the generated secret removes account access
- verify the secret panel disappears only after acknowledgement

Diffstat:
Mapp/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt | 38++++++++++++++++++++++++++++++++++++++
Mapp/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt | 6++++++
Mapp/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt | 28++++++++++++++++++++++++++++
Mdocs/implementation/nostr-runtime-rcld.md | 2+-
4 files changed, 73 insertions(+), 1 deletion(-)

diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt b/app/desktop/src/main/kotlin/org/radroots/studio/accounts/ui/StudioScreen.kt @@ -30,6 +30,8 @@ data class StudioUiActions( val editImportDraft: (String) -> Unit = {}, val generateAccount: () -> Unit = {}, val importSecretKey: () -> Unit = {}, + val copyText: (String) -> Unit = {}, + val acknowledgeGeneratedKeyBackup: () -> Unit = {}, ) @Composable @@ -91,6 +93,10 @@ private fun InactiveAccountsScreen( onClick = actions.importSecretKey, ) + model.generatedKeyBackup?.let { backup -> + GeneratedKeyBackupPanel(backup, actions) + } + model.problem?.let { BasicText(it, Modifier.testTag("accounts-problem")) } @@ -102,6 +108,38 @@ private fun InactiveAccountsScreen( } @Composable +private fun GeneratedKeyBackupPanel( + backup: GeneratedKeyBackupUiModel, + actions: StudioUiActions, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .background(InputBackgroundColor) + .padding(12.dp) + .testTag("generated-key-backup"), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + BasicText("Save this key") + BasicText("Losing this secret key means losing access to the account.") + BasicText(backup.npub) + BasicText(backup.nsec, Modifier.testTag("generated-nsec")) + TextAction( + text = "Copy", + testTag = "copy-generated-key", + contentDescription = "Copy generated Nostr secret key", + onClick = { actions.copyText(backup.nsec) }, + ) + TextAction( + text = "I have saved this key", + testTag = "acknowledge-key-backup", + contentDescription = "Confirm generated key backup", + onClick = actions.acknowledgeGeneratedKeyBackup, + ) + } +} + +@Composable internal fun TextAction( text: String, testTag: String, diff --git a/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt b/app/desktop/src/main/kotlin/org/radroots/studio/application/RadrootsApplication.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection import kotlinx.coroutines.CoroutineScope import org.radroots.studio.accounts.ui.StudioScreen import org.radroots.studio.accounts.ui.StudioUiActions @@ -29,6 +31,10 @@ fun RadrootsApplication( editImportDraft = store::editImportDraft, generateAccount = store::generateAccount, importSecretKey = store::importSecretKey, + copyText = { value -> + Toolkit.getDefaultToolkit().systemClipboard.setContents(StringSelection(value), null) + }, + acknowledgeGeneratedKeyBackup = store::acknowledgeGeneratedKeyBackup, ), ) } diff --git a/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt b/app/desktop/src/test/kotlin/org/radroots/studio/accounts/ui/StudioScreenTest.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.onAllNodesWithTag import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick @@ -59,6 +61,32 @@ class StudioScreenTest { onNodeWithText("The secret key is invalid.").assertIsDisplayed() onNodeWithTag("accounts-empty").assertIsDisplayed() } + + @Test + fun generatedKeyBackupCopiesAndClearsOnlyAfterAcknowledgement() = runComposeUiTest { + var backup: GeneratedKeyBackupUiModel? by mutableStateOf( + GeneratedKeyBackupUiModel("npub1generated", "nsec1generated"), + ) + var copied: String? = null + setContent { + StudioScreen( + model = emptyUiModel().copy(generatedKeyBackup = backup), + actions = StudioUiActions( + copyText = { copied = it }, + acknowledgeGeneratedKeyBackup = { backup = null }, + ), + ) + } + + onNodeWithTag("generated-key-backup").assertIsDisplayed() + onNodeWithTag("generated-nsec").assertIsDisplayed() + onNodeWithTag("copy-generated-key").performClick() + assertEquals("nsec1generated", copied) + + onNodeWithTag("acknowledge-key-backup").performClick() + onAllNodesWithTag("generated-key-backup").assertCountEquals(0) + onAllNodesWithTag("generated-nsec").assertCountEquals(0) + } } private fun emptyUiModel( diff --git a/docs/implementation/nostr-runtime-rcld.md b/docs/implementation/nostr-runtime-rcld.md @@ -535,7 +535,7 @@ handoff commit sequence. - [x] 49. Bootstrap AppCore once at application root. - [x] 50. Create UI model mapping helpers without server fields. - [x] 51. Implement inactive account screen generate/import controls. -- [ ] 52. Implement generated-key backup panel. +- [x] 52. Implement generated-key backup panel. - [ ] 53. Implement saved-account list, activation, and removal controls. - [ ] 54. Implement active account home screen. - [ ] 55. Implement switch account flow in UI.