[ui] upload modal: prompt for the nsec passphrase instead of dead-ending after reload

With a persisted ("remember this key") nsec login, uploading a video after a page
reload reported "storage is locked" and then "No signer available to unlock storage."
The reload restores the logged-in pubkey but NOT the in-memory signer (the key is
passphrase-encrypted), and UploadModal.handleUnlock dead-ended on the alert — it never
asked for the passphrase.

Fix (mirrors the storage pane's #36 behavior): handleUnlock now calls
promptStoredNsecUnlock(pubkey) when there's no usable signer. If the active account has
a locked saved nsec key (getStoredSessionActorMetadata), it shows an actionable message
and opens the login modal's existing unlock-saved-key (passphrase) flow via a new
onRequestUnlock hook (wired in initUploadModal -> app.loginModalController.openModal),
instead of the dead-end alert. After the user re-enters their passphrase the signer is
active and the upload proceeds.

Tests: tests/upload-modal-nsec-unlock.test.mjs (5 — detects locked nsec for the active
account + opens unlock; ignores missing/non-nsec/other-account; falls through with no
hook). Build + lint clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-30 15:24:29 -04:00
1 parent 3e57ab2ea9
commit f00d2b5f49
4 files changed
+144

No files matched your search

+48
View File
@@ -53,6 +53,7 @@ export class UploadModal {
eventTarget,
container,
onRequestStorageSettings,
onRequestUnlock,
} = {}) {
this.authService = authService || null;
this.r2Service = r2Service || null;
@@ -88,6 +89,9 @@ export class UploadModal {
eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
this.container = container || null;
this.onRequestStorageSettings = typeof onRequestStorageSettings === "function" ? onRequestStorageSettings : null;
// Opens the login modal's unlock-saved-key (passphrase) flow — used when a
// persisted nsec session is locked after a reload.
this.onRequestUnlock = typeof onRequestUnlock === "function" ? onRequestUnlock : null;
this.root = null;
this.isVisible = false;
@@ -813,6 +817,13 @@ export class UploadModal {
? signer.canSign()
: typeof signer?.signEvent === "function";
if (!canSign) {
// A persisted ("remember this key") nsec session restores the logged-in
// pubkey after a reload but NOT the in-memory signer (the key is passphrase-
// encrypted). Prompt the user to re-unlock their saved key instead of dead-
// ending — matches the storage pane's behavior (#36).
if (this.promptStoredNsecUnlock(pubkey)) {
return;
}
alert("No signer available to unlock storage.");
return;
}
@@ -844,6 +855,43 @@ export class UploadModal {
}
}
// Returns true if a locked persisted-nsec session was detected (for the active
// account) and the re-unlock (passphrase) flow was opened, so the caller can stop.
promptStoredNsecUnlock(pubkey) {
const client = this.authService?.nostrClient;
if (!client || typeof client.getStoredSessionActorMetadata !== "function") {
return false;
}
let meta = null;
try {
meta = client.getStoredSessionActorMetadata();
} catch (err) {
return false;
}
if (!meta || meta.hasEncryptedKey !== true || meta.source !== "nsec") {
return false;
}
const metaPubkey =
typeof meta.pubkey === "string" ? meta.pubkey.trim().toLowerCase() : "";
const activePubkey =
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
if (activePubkey && metaPubkey && metaPubkey !== activePubkey) {
return false;
}
if (typeof this.onRequestUnlock !== "function") {
return false;
}
this.showError(
"Your saved key is locked after reloading. Re-enter your passphrase to unlock it, then try the upload again.",
);
try {
this.onRequestUnlock();
} catch (err) {
userLogger.warn("[UploadModal] Failed to open saved-key unlock flow:", err);
}
return true;
}
updateLockUi() {
const locked = !this.isStorageUnlocked;
if (this.statusText?.storageLock) {