mirror of
https://github.com/hoornet/vega.git
synced 2026-06-08 14:11:55 -07:00
polish: onboarding step indicator + safe backup escape hatch
- Onboarding Shell: larger accent VEGA wordmark + step-progress dots across the create -> backup -> interests path. - Backup step gains an "I'll back up my key later" escape hatch. It runs the SAME keychain-saving login as confirming (skips only the checkbox) — wiring it to onComplete alone would have lost the generated key. - Settings -> Identity now reveals the secret key (nsec): reveal/hide/ copy via the existing load_nsec keychain command, hidden for read-only and remote-signer accounts. This makes the escape hatch honest — its note truthfully points users to where they can retrieve the key, and fills a real gap (the nsec was previously unreachable after onboarding).
This commit is contained in:
@@ -10,11 +10,36 @@ interface OnboardingFlowProps {
|
||||
|
||||
// ─── Shared layout ───────────────────────────────────────────────────────────
|
||||
|
||||
function Shell({ children }: { children: React.ReactNode }) {
|
||||
function Shell({
|
||||
children,
|
||||
step,
|
||||
totalSteps,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
step?: number;
|
||||
totalSteps?: number;
|
||||
}) {
|
||||
return (
|
||||
<div className="h-screen w-screen bg-bg flex items-center justify-center">
|
||||
<div className="w-full max-w-md px-8">
|
||||
<div className="text-text-dim text-[10px] font-bold tracking-[0.3em] uppercase mb-8">VEGA</div>
|
||||
{/* Logo wordmark */}
|
||||
<div className="text-accent text-[18px] font-bold tracking-[0.25em] mb-2 select-none">VEGA</div>
|
||||
|
||||
{/* Step dots — shown only for the linear create→backup→interests path */}
|
||||
{step !== undefined && totalSteps !== undefined && (
|
||||
<div className="flex gap-1.5 mb-8">
|
||||
{Array.from({ length: totalSteps }).map((_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`h-1 rounded-full transition-all duration-300 ${
|
||||
i < step ? "bg-accent w-4" : i === step ? "bg-accent w-6" : "bg-border w-4"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step === undefined && <div className="mb-8" />}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +95,7 @@ function InterestsStep({ onComplete }: { onComplete: () => void }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<Shell step={2} totalSteps={3}>
|
||||
<Heading>What are you into?</Heading>
|
||||
<Body>Pick a few topics to get your feed started. You can always change this later.</Body>
|
||||
|
||||
@@ -157,7 +182,7 @@ function CreateStep({ onNext }: { onNext: (signer: NDKPrivateKeySigner) => void
|
||||
};
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<Shell step={0} totalSteps={3}>
|
||||
<Heading>Your identity is ready.</Heading>
|
||||
<Body>
|
||||
We generated a unique key pair for you. Your <strong className="text-text">public key</strong> is
|
||||
@@ -215,8 +240,18 @@ function BackupStep({ signer, onComplete }: { signer: NDKPrivateKeySigner; onCom
|
||||
onComplete();
|
||||
};
|
||||
|
||||
// "Back up later" escape hatch — runs the SAME login as confirming (this is
|
||||
// what saves the key to the keychain and signs in); it only skips the
|
||||
// checkbox gate. The key stays retrievable via Settings → Identity.
|
||||
const handleSkipBackup = async () => {
|
||||
setSaving(true);
|
||||
await loginWithNsec(signer.nsec);
|
||||
setSaving(false);
|
||||
onComplete();
|
||||
};
|
||||
|
||||
return (
|
||||
<Shell>
|
||||
<Shell step={1} totalSteps={3}>
|
||||
<Heading>Save your secret key.</Heading>
|
||||
<Body>
|
||||
Your <strong className="text-text">secret key</strong> is the only way to recover your
|
||||
@@ -272,6 +307,16 @@ function BackupStep({ signer, onComplete }: { signer: NDKPrivateKeySigner; onCom
|
||||
>
|
||||
{saving ? "Setting up…" : "Start using Vega"}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSkipBackup}
|
||||
disabled={saving}
|
||||
className="w-full py-2 text-[11px] text-text-dim hover:text-text transition-colors mt-1 disabled:opacity-30"
|
||||
>
|
||||
I'll back up my key later
|
||||
</button>
|
||||
<p className="text-warning text-[10px] text-center mt-1 opacity-70">
|
||||
You can reveal your key any time in Settings → Identity.
|
||||
</p>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { writeTextFile } from "@tauri-apps/plugin-fs";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useUserStore } from "../../stores/user";
|
||||
import { useUIStore } from "../../stores/ui";
|
||||
import { useWoTStore } from "../../stores/wot";
|
||||
@@ -206,8 +207,18 @@ function WoTSection() {
|
||||
}
|
||||
|
||||
function IdentitySection() {
|
||||
const { npub, loggedIn } = useUserStore();
|
||||
const { npub, loggedIn, pubkey } = useUserStore();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [nsec, setNsec] = useState<string | null>(null);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const [nsecCopied, setNsecCopied] = useState(false);
|
||||
|
||||
// Load the secret key from the OS keychain. Returns null for read-only
|
||||
// (npub) and remote-signer (bunker) accounts — the nsec row is hidden then.
|
||||
useEffect(() => {
|
||||
if (!pubkey) return;
|
||||
invoke<string | null>("load_nsec", { pubkey }).then(setNsec).catch(() => setNsec(null));
|
||||
}, [pubkey]);
|
||||
|
||||
if (!loggedIn || !npub) {
|
||||
return (
|
||||
@@ -225,6 +236,14 @@ function IdentitySection() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopyNsec = () => {
|
||||
if (!nsec) return;
|
||||
navigator.clipboard.writeText(nsec).then(() => {
|
||||
setNsecCopied(true);
|
||||
setTimeout(() => setNsecCopied(false), 2000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">Identity</h2>
|
||||
@@ -238,6 +257,33 @@ function IdentitySection() {
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-text-dim text-[10px] mt-1 px-1">Your public key. Safe to share.</p>
|
||||
|
||||
{nsec && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 px-3 py-2 border border-danger/40 mt-3">
|
||||
<span className={`font-mono text-[11px] truncate flex-1 ${revealed ? "text-text" : "text-text-dim"}`}>
|
||||
{revealed ? nsec : "•".repeat(48)}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setRevealed((v) => !v)}
|
||||
className="text-[10px] text-text-dim hover:text-text transition-colors shrink-0"
|
||||
>
|
||||
{revealed ? "hide" : "reveal"}
|
||||
</button>
|
||||
{revealed && (
|
||||
<button
|
||||
onClick={handleCopyNsec}
|
||||
className="text-[10px] text-text-dim hover:text-danger transition-colors shrink-0"
|
||||
>
|
||||
{nsecCopied ? "copied ✓" : "copy"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-text-dim text-[10px] mt-1 px-1">
|
||||
Your secret key — the only way to recover this account. Never share it. Back it up somewhere safe.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user