diff --git a/src/App.tsx b/src/App.tsx index e8ed9ad..c5cbcc6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Sidebar } from "./components/sidebar/Sidebar"; import { Feed } from "./components/feed/Feed"; import { SearchView } from "./components/search/SearchView"; @@ -6,10 +7,18 @@ import { SettingsView } from "./components/shared/SettingsView"; import { ProfileView } from "./components/profile/ProfileView"; import { ThreadView } from "./components/thread/ThreadView"; import { ArticleEditor } from "./components/article/ArticleEditor"; +import { OnboardingFlow } from "./components/onboarding/OnboardingFlow"; import { useUIStore } from "./stores/ui"; function App() { const currentView = useUIStore((s) => s.currentView); + const [onboardingDone, setOnboardingDone] = useState( + () => !!localStorage.getItem("wrystr_pubkey") + ); + + if (!onboardingDone) { + return setOnboardingDone(true)} />; + } return (
diff --git a/src/components/onboarding/OnboardingFlow.tsx b/src/components/onboarding/OnboardingFlow.tsx new file mode 100644 index 0000000..cea5875 --- /dev/null +++ b/src/components/onboarding/OnboardingFlow.tsx @@ -0,0 +1,302 @@ +import { useState, useEffect } from "react"; +import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; +import { useUserStore } from "../../stores/user"; + +type Step = "welcome" | "create" | "backup" | "login"; + +interface OnboardingFlowProps { + onComplete: () => void; +} + +// ─── Shared layout ─────────────────────────────────────────────────────────── + +function Shell({ children }: { children: React.ReactNode }) { + return ( +
+
+
WRYSTR
+ {children} +
+
+ ); +} + +function Heading({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +function Body({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +// ─── Step: Welcome ─────────────────────────────────────────────────────────── + +function WelcomeStep({ onCreateNew, onHaveKey }: { onCreateNew: () => void; onHaveKey: () => void }) { + return ( + + Welcome to Wrystr. + + Wrystr is a Nostr client — a social platform where you own your identity, + your content, and your social graph. No company can delete your account or + censor your posts. + + + To get started, you need a key pair. Think of it like a username and password + combined into one — except you control it completely. + +
+ + +
+
+ ); +} + +// ─── Step: Create key ──────────────────────────────────────────────────────── + +function CreateStep({ onNext }: { onNext: (signer: NDKPrivateKeySigner) => void }) { + const [signer] = useState(() => NDKPrivateKeySigner.generate()); + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(signer.npub).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + return ( + + Your identity is ready. + + We generated a unique key pair for you. Your public key is + your identity on Nostr — like a username, but cryptographically yours. Share it freely. + + +
+
+ Your public key (npub) +
+
+ {signer.npub} + +
+
+

Safe to share with anyone. This is how people find you on Nostr.

+ + +
+ ); +} + +// ─── Step: Backup nsec ─────────────────────────────────────────────────────── + +function BackupStep({ signer, onComplete }: { signer: NDKPrivateKeySigner; onComplete: () => void }) { + const { loginWithNsec, loginError } = useUserStore(); + const [confirmed, setConfirmed] = useState(false); + const [copied, setCopied] = useState(false); + const [saving, setSaving] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(signer.nsec).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 3000); + }); + }; + + const handleStart = async () => { + if (!confirmed) return; + setSaving(true); + await loginWithNsec(signer.nsec); + setSaving(false); + onComplete(); + }; + + return ( + + Save your secret key. + + Your secret key is the only way to recover your + account. Save it in a password manager, notes app, or write it down. Wrystr never + stores it — if you lose it, your account is gone. + + +
+
+ Secret key (nsec) — keep private +
+
+ {signer.nsec} + +
+
+

+ Never share this with anyone. Anyone who has it controls your account. +

+ + + + {loginError &&

{loginError}

} + + +
+ ); +} + +// ─── Step: Login with existing key ─────────────────────────────────────────── + +function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () => void }) { + const { loginWithNsec, loginWithPubkey, loginError, loggedIn } = useUserStore(); + const [mode, setMode] = useState<"nsec" | "npub">("nsec"); + const [value, setValue] = useState(""); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (loggedIn) onComplete(); + }, [loggedIn]); + + const handleLogin = async () => { + if (!value.trim() || loading) return; + setLoading(true); + if (mode === "nsec") { + await loginWithNsec(value.trim()); + } else { + await loginWithPubkey(value.trim()); + } + setLoading(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") handleLogin(); + }; + + return ( + + Log in with your key. + +
+ {(["nsec", "npub"] as const).map((m) => ( + + ))} +
+ + setValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={mode === "nsec" ? "nsec1…" : "npub1…"} + autoFocus + className="w-full bg-bg border border-border px-3 py-2 text-text text-[12px] font-mono focus:outline-none focus:border-accent/50 placeholder:text-text-dim mb-2" + style={{ WebkitUserSelect: "text", userSelect: "text" } as React.CSSProperties} + /> + + {mode === "npub" && ( +

Read-only mode — you can browse but not post, react, or zap.

+ )} + + {loginError &&

{loginError}

} + +
+ + +
+
+ ); +} + +// ─── Orchestrator ──────────────────────────────────────────────────────────── + +export function OnboardingFlow({ onComplete }: OnboardingFlowProps) { + const [step, setStep] = useState("welcome"); + const [generatedSigner, setGeneratedSigner] = useState(null); + + if (step === "welcome") { + return ( + setStep("create")} + onHaveKey={() => setStep("login")} + /> + ); + } + + if (step === "create") { + return ( + { + setGeneratedSigner(signer); + setStep("backup"); + }} + /> + ); + } + + if (step === "backup" && generatedSigner) { + return ; + } + + if (step === "login") { + return setStep("welcome")} onComplete={onComplete} />; + } + + return null; +}