mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-10-27 08:31:21 +00:00
Stamp genuine successes with the brand's brass ring + checkmark
Signing in, creating an account, recovering one, and rotating a
password all used to just silently swap to the dashboard screen --
nothing marked the moment as a success versus any other navigation.
These are exactly the security-relevant "you're safely in/done"
moments, so mark them with the same brass ring + checkmark mark used
in the logo: a brief overlay stamps the destination screen with the
mark and a short label ("Signed in", "Account created", "Account
recovered", "Password updated"), then fades on its own.
Deliberately narrow: this only fires on those four real completions,
not on every validation step or plain navigation (e.g. the "Back to
dashboard" button), so it stays a rare, meaningful beat instead of
becoming visual noise. It's also inert by design -- pointer-events:
none means it can never block interacting with the screen underneath,
a JS timer (not just the CSS animation) guarantees it's removed even
if the animation can't run, and prefers-reduced-motion drops the
motion but keeps the same brief hold-then-clear timing.
This commit is contained in:
1 parent
f725e2cd83
commit
ed8d8eb1be
2 files changed
+101
-5
No files matched your search
@@ -81,6 +81,11 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
private exportedNsec = "";
|
||||
private changePasswordNewCredential = "";
|
||||
|
||||
// Brief animated brass-seal stamp (matching the widget's own brand mark) shown once over
|
||||
// the destination screen right after a real security-relevant success -- see flashSuccess().
|
||||
private pendingSuccessLabel: string | null = null;
|
||||
private successDismissTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Rollback confirmation (§16.2 step 6). A RollbackDetectedError from either login or
|
||||
// password-change means this device has already seen a newer credential generation than the
|
||||
// one just read -- most likely a rotated-away password being replayed from a relay that never
|
||||
@@ -124,6 +129,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
if (this.successDismissTimer !== null) clearTimeout(this.successDismissTimer);
|
||||
this.releaseSigner();
|
||||
this.worker.terminate();
|
||||
}
|
||||
@@ -629,7 +635,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
this.sessionWarnings = [];
|
||||
this.noteSignerClaim(this.claimSigner());
|
||||
this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: this.session?.publicKey } }));
|
||||
this.goto("dashboard");
|
||||
this.flashSuccess("dashboard", "Account created");
|
||||
}
|
||||
|
||||
private async handleLoginSubmit(): Promise<void> {
|
||||
@@ -653,7 +659,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
this.busy = false;
|
||||
this.noteSignerClaim(this.claimSigner());
|
||||
this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: result.everydayPublicKey } }));
|
||||
this.goto("dashboard");
|
||||
this.flashSuccess("dashboard", "Signed in");
|
||||
void this.offerToSaveCredential(loginName, password);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === "RollbackDetectedError") {
|
||||
@@ -725,7 +731,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
this.sessionWarnings = [];
|
||||
this.noteSignerClaim(this.claimSigner());
|
||||
this.dispatchEvent(new CustomEvent("bitlogin-login", { detail: { publicKey: this.session?.publicKey } }));
|
||||
this.goto("dashboard");
|
||||
this.flashSuccess("dashboard", "Account recovered");
|
||||
void this.offerToSaveCredential(newLoginName, newPassword);
|
||||
}
|
||||
|
||||
@@ -757,7 +763,7 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
this.busy = false;
|
||||
this.sessionWarnings = [];
|
||||
this.noteSignerClaim(this.claimSigner());
|
||||
this.goto("dashboard");
|
||||
this.flashSuccess("dashboard", "Password updated");
|
||||
void this.offerToSaveCredential(this.loginName, newPassword);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.name === "RollbackDetectedError") {
|
||||
@@ -795,7 +801,48 @@ export class BitLoginAuthElement extends HTMLElement {
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
this.root.innerHTML = `<style>${WIDGET_STYLES}</style><div class="card">${this.renderScreen()}</div>`;
|
||||
const successOverlay = this.pendingSuccessLabel ? this.renderSuccessOverlay(this.pendingSuccessLabel) : "";
|
||||
this.root.innerHTML = `<style>${WIDGET_STYLES}</style><div class="card">${this.renderScreen()}${successOverlay}</div>`;
|
||||
}
|
||||
|
||||
private renderSuccessOverlay(label: string): string {
|
||||
return `
|
||||
<div class="success-overlay" data-success-overlay>
|
||||
<span class="success-stamp">
|
||||
<svg viewBox="0 0 64 64" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="bl-success-brass" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#e6c481" />
|
||||
<stop offset="1" stop-color="#b3924a" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="32" cy="32" r="21" fill="none" stroke="url(#bl-success-brass)" stroke-width="2.5" />
|
||||
<path d="M27 32.5l3.6 3.6L38 28.2" fill="none" stroke="url(#bl-success-brass)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<p class="success-label">${escapeHtml(label)}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to `screen` and briefly stamps it with the same brass-ring-and-checkmark mark
|
||||
* used in the logo, for the handful of moments that are a genuine security-relevant success
|
||||
* -- signing in, creating an account, recovering one, or rotating a password -- rather than
|
||||
* every validation step or navigation. Purely decorative: `pointer-events: none` (see
|
||||
* styles.ts) means it never blocks interacting with the screen underneath, and a JS timer
|
||||
* (not just the CSS animation) guarantees it's removed even if the animation can't run.
|
||||
*/
|
||||
private flashSuccess(screen: Screen, label: string): void {
|
||||
this.goto(screen);
|
||||
if (this.successDismissTimer !== null) clearTimeout(this.successDismissTimer);
|
||||
this.pendingSuccessLabel = label;
|
||||
this.render();
|
||||
this.successDismissTimer = setTimeout(() => {
|
||||
this.pendingSuccessLabel = null;
|
||||
this.successDismissTimer = null;
|
||||
this.root.querySelector("[data-success-overlay]")?.remove();
|
||||
}, 1100);
|
||||
}
|
||||
|
||||
private renderError(): string {
|
||||
|
||||
@@ -54,6 +54,7 @@ export const WIDGET_STYLES = /* css */ `
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: var(--bl-bg);
|
||||
color: var(--bl-fg);
|
||||
@@ -79,6 +80,54 @@ h2 {
|
||||
margin: 0 0 6px;
|
||||
color: var(--bl-fg);
|
||||
}
|
||||
/* Brief confirmation stamp shown over the destination screen right after a real
|
||||
security-relevant success (see flashSuccess() in element.ts) -- the same brass ring
|
||||
and checkmark as the brand mark, so a successful sign-in/create/recover/rotate reads
|
||||
as "sealed," not just a silent screen change. Never blocks interaction, and fades out
|
||||
on its own; the JS timer that removes it entirely doesn't depend on the animation. */
|
||||
.success-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
background: var(--bl-bg);
|
||||
border-radius: var(--bl-radius);
|
||||
pointer-events: none;
|
||||
animation: bl-overlay-fade-out 0.4s ease 0.7s both;
|
||||
}
|
||||
.success-stamp svg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
animation: bl-stamp-in 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both;
|
||||
}
|
||||
.success-label {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--bl-fg);
|
||||
opacity: 0;
|
||||
animation: bl-label-in 0.3s ease 0.35s both;
|
||||
}
|
||||
@keyframes bl-stamp-in {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
60% { transform: scale(1.15); opacity: 1; }
|
||||
100% { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
@keyframes bl-label-in {
|
||||
to { opacity: 1; }
|
||||
}
|
||||
@keyframes bl-overlay-fade-out {
|
||||
0% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.success-overlay { animation: none; }
|
||||
.success-stamp svg { animation: none; }
|
||||
.success-label { animation: none; opacity: 1; }
|
||||
}
|
||||
p.sub {
|
||||
color: var(--bl-muted);
|
||||
margin: 0 0 18px;
|
||||
|
||||
Reference in new issue
Block a user