mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
[ui] #36: auto-open the unlock-saved-key flow from storage when an nsec session is locked
Follow-up to the locked-nsec detection: clicking "Unlock Storage" on a locked
persisted-nsec session (no in-memory signer after a page reload) now auto-opens the
login modal straight to its existing unlock-saved-key (passphrase) flow, so the user
re-unlocks in one step instead of hunting for the Login menu. Once unlocked, the
signer is active app-wide and re-opening the storage pane auto-unlocks.
- applicationBootstrap: add a side-effect-free openLoginModal service to
profileModalServices (app.loginModalController.openModal — no add-account callback;
idempotently ensures the modal is initialized first).
- ProfileStorageController: reportLockedNsecSession({ autoOpenLogin }) opens the modal
only from the user-initiated unlock click, never from the passive pane render (no
surprise popups).
- Tests extended to 8 (auto-open fires on click; stays closed on passive render).
Build + lint clean.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
a669040740
commit
35bb1bc576
4 files changed
+77
-9
No files matched your search
@@ -543,6 +543,33 @@ export default class ApplicationBootstrap {
|
||||
authService: app.authService,
|
||||
requestAddProfileLogin: (options) =>
|
||||
app.requestProfileAdditionLogin(options),
|
||||
// Side-effect-free open of the login modal (no add-account callback). Used by
|
||||
// the storage pane to route a locked persisted-nsec session to the existing
|
||||
// unlock-saved-key (passphrase) flow. Returns true if the modal opened.
|
||||
openLoginModal: (options = {}) => {
|
||||
try {
|
||||
if (typeof app.initializeLoginModalController === "function") {
|
||||
app.initializeLoginModalController();
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to ensure login modal before open:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
const controller = app.loginModalController;
|
||||
if (controller && typeof controller.openModal === "function") {
|
||||
try {
|
||||
return controller.openModal(options) !== false;
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[Application] Failed to open login modal:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
describeLoginError: (error, fallbackMessage) =>
|
||||
app.describeLoginError(error, fallbackMessage),
|
||||
hashtagPreferences: app.hashtagPreferences,
|
||||
|
||||
@@ -517,7 +517,7 @@ export class ProfileStorageController {
|
||||
return meta;
|
||||
}
|
||||
|
||||
reportLockedNsecSession() {
|
||||
reportLockedNsecSession({ autoOpenLogin = false } = {}) {
|
||||
userLogger.info(
|
||||
"[storage-unlock] saved nsec key is locked (no in-memory signer after reload); prompting re-unlock.",
|
||||
);
|
||||
@@ -527,6 +527,25 @@ export class ProfileStorageController {
|
||||
error.code = "storage-unlock-locked-nsec-session";
|
||||
this.setStorageUnlockFailureState(error);
|
||||
this.mainController.showError(this.getStorageUnlockFailureMessage(error));
|
||||
|
||||
// When the user actively clicked "Unlock Storage", open the login modal straight
|
||||
// to its existing unlock-saved-key (passphrase) flow so they can re-unlock in one
|
||||
// step. After they unlock, the signer is active app-wide and re-opening the storage
|
||||
// pane auto-unlocks. We don't auto-open from the passive pane render (would be
|
||||
// surprising); the status text + button still guide them there.
|
||||
if (autoOpenLogin) {
|
||||
const openLoginModal = this.mainController.services?.openLoginModal;
|
||||
if (typeof openLoginModal === "function") {
|
||||
try {
|
||||
openLoginModal();
|
||||
} catch (openError) {
|
||||
devLogger.warn(
|
||||
"[ProfileModal] Failed to open login modal for saved-key unlock:",
|
||||
openError,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async requestStorageUnlockPermissions() {
|
||||
@@ -570,7 +589,7 @@ export class ProfileStorageController {
|
||||
|
||||
if (!signer) {
|
||||
if (this.getLockedStoredNsecSession(pubkey)) {
|
||||
this.reportLockedNsecSession();
|
||||
this.reportLockedNsecSession({ autoOpenLogin: true });
|
||||
return;
|
||||
}
|
||||
this.mainController.showError("No active signer found. Please login.");
|
||||
@@ -647,7 +666,7 @@ export class ProfileStorageController {
|
||||
// A restored-but-locked persisted nsec session can leave a decrypt-less stub
|
||||
// signer; route it to the re-unlock guidance rather than the generic message.
|
||||
if (this.getLockedStoredNsecSession(pubkey)) {
|
||||
this.reportLockedNsecSession();
|
||||
this.reportLockedNsecSession({ autoOpenLogin: true });
|
||||
return;
|
||||
}
|
||||
const missingDecryptError = new Error(
|
||||
|
||||
@@ -20,6 +20,7 @@ const OTHER_PUBKEY = "b".repeat(64);
|
||||
|
||||
function makeController({ meta } = {}) {
|
||||
const shownErrors = [];
|
||||
const loginOpens = [];
|
||||
const mainController = {
|
||||
normalizeHexPubkey: (value) =>
|
||||
typeof value === "string" ? value.trim().toLowerCase() : "",
|
||||
@@ -29,10 +30,14 @@ function makeController({ meta } = {}) {
|
||||
nostrClient: {
|
||||
getStoredSessionActorMetadata: () => meta,
|
||||
},
|
||||
openLoginModal: (options) => {
|
||||
loginOpens.push(options ?? null);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
const controller = new ProfileStorageController(mainController);
|
||||
return { controller, shownErrors };
|
||||
return { controller, shownErrors, loginOpens };
|
||||
}
|
||||
|
||||
const persistedNsecMeta = {
|
||||
@@ -81,6 +86,18 @@ test("reporting the locked session shows the actionable re-unlock message, not a
|
||||
assert.doesNotMatch(msg, /no active signer/i);
|
||||
});
|
||||
|
||||
test("autoOpenLogin opens the login modal's unlock flow (one-click re-unlock)", () => {
|
||||
const { controller, loginOpens } = makeController({ meta: persistedNsecMeta });
|
||||
controller.reportLockedNsecSession({ autoOpenLogin: true });
|
||||
assert.equal(loginOpens.length, 1, "should open the login modal exactly once");
|
||||
});
|
||||
|
||||
test("the passive path does NOT auto-open the login modal (no surprise popups)", () => {
|
||||
const { controller, loginOpens } = makeController({ meta: persistedNsecMeta });
|
||||
controller.reportLockedNsecSession();
|
||||
assert.equal(loginOpens.length, 0, "default (passive) report must not open a modal");
|
||||
});
|
||||
|
||||
test("the new error code maps to a saved-key/passphrase message", () => {
|
||||
const { controller } = makeController({ meta: persistedNsecMeta });
|
||||
const msg = controller.getStorageUnlockFailureMessage({
|
||||
|
||||
@@ -814,11 +814,16 @@ Reported 2026-06-25. Relates to #17 (NIP-71 interop) / the bitvid→NIP-71 mirro
|
||||
no-signer / no-decryptor messages. New error code
|
||||
`storage-unlock-locked-nsec-session`. Tests:
|
||||
`tests/storage-unlock-locked-nsec.test.mjs` (6, behavioral). Build + lint clean.
|
||||
- [ ] **Follow-up (UX nicety):** auto-open the login modal's existing unlock-saved-key
|
||||
(passphrase) flow directly from the storage pane so the user re-unlocks in one
|
||||
click instead of navigating to Login — needs a side-effect-free
|
||||
`requestUnlockStoredSession` hook (the current `requestAddProfileLogin` path runs an
|
||||
add-account callback). Deferred to keep this fix small.
|
||||
- [x] **Follow-up (UX nicety) — DONE 2026-06-30.** Clicking "Unlock Storage" on a
|
||||
locked persisted-nsec session now **auto-opens the login modal** straight to its
|
||||
existing unlock-saved-key (passphrase) flow, so the user re-unlocks in one step.
|
||||
Added a side-effect-free `openLoginModal` service to `profileModalServices`
|
||||
(`app.loginModalController.openModal` — no add-account callback, idempotently
|
||||
ensures the modal exists first); `reportLockedNsecSession({ autoOpenLogin })` calls
|
||||
it only from the user-initiated unlock click, NOT the passive pane render (no
|
||||
surprise popups). After the user unlocks, the signer is active app-wide and
|
||||
re-opening the storage pane auto-unlocks. Tests extended (8 total: asserts auto-open
|
||||
fires on click, stays closed on passive render).
|
||||
- [ ] **VERIFY on unstable:** nsec-login with "remember key" + passphrase, reload the
|
||||
page, open Profile → Storage → the status should read the actionable re-unlock hint
|
||||
(not "No active signer"); after re-entering the passphrase via Login, storage
|
||||
|
||||
Reference in new issue
Block a user