diff --git a/kez-chat/web/src/App.svelte b/kez-chat/web/src/App.svelte index 736b89a..d5c1a5b 100644 --- a/kez-chat/web/src/App.svelte +++ b/kez-chat/web/src/App.svelte @@ -10,6 +10,7 @@ import CreateAccount from "./routes/CreateAccount.svelte"; import Restore from "./routes/Restore.svelte"; import Unlock from "./routes/Unlock.svelte"; + import Welcome from "./routes/Welcome.svelte"; import Identity from "./routes/Identity.svelte"; import Claims from "./routes/Claims.svelte"; import AddClaim from "./routes/AddClaim.svelte"; @@ -21,6 +22,7 @@ "/create": CreateAccount, "/restore": Restore, "/unlock": Unlock, + "/welcome": Welcome, "/chats": Messages, "/identity": Identity, "/claims": Claims, @@ -29,7 +31,7 @@ }; // App routes show the nav chrome; everything else (auth flow) is full-bleed. - const APP_ROUTES = ["/chats", "/identity", "/claims", "/claims/add", "/settings"]; + const APP_ROUTES = ["/welcome", "/chats", "/identity", "/claims", "/claims/add", "/settings"]; const showNav = $derived(!!session.unlocked && APP_ROUTES.includes($location)); onMount(async () => { diff --git a/kez-chat/web/src/lib/onboarding.svelte.ts b/kez-chat/web/src/lib/onboarding.svelte.ts new file mode 100644 index 0000000..4e0f551 --- /dev/null +++ b/kez-chat/web/src/lib/onboarding.svelte.ts @@ -0,0 +1,48 @@ +// First-run onboarding state. Two persisted flags: +// • onboarded — user finished/skipped the Getting Started checklist +// • seedAcked — user confirmed they backed up their recovery seed +// Both in localStorage; exposed as Svelte 5 $state so the "finish setup" +// nudge reactively disappears. Everything else in the checklist derives +// from real app state (claims, biometric, notification permission). + +const ONBOARDED = "kez-chat:onboarded"; +const SEED_ACKED = "kez-chat:seed_acked"; + +function read(key: string): boolean { + try { + return localStorage.getItem(key) === "1"; + } catch { + return false; + } +} +function write(key: string, val: boolean) { + try { + if (val) localStorage.setItem(key, "1"); + else localStorage.removeItem(key); + } catch { + /* private mode — fine */ + } +} + +class Onboarding { + onboarded = $state(read(ONBOARDED)); + seedAcked = $state(read(SEED_ACKED)); + + finish() { + this.onboarded = true; + write(ONBOARDED, true); + } + + ackSeed() { + this.seedAcked = true; + write(SEED_ACKED, true); + } + + /** Re-open the checklist (e.g. from Settings → Getting started). */ + reopen() { + this.onboarded = false; + write(ONBOARDED, false); + } +} + +export const onboarding = new Onboarding(); diff --git a/kez-chat/web/src/routes/Messages.svelte b/kez-chat/web/src/routes/Messages.svelte index fd0c2d3..2d80e95 100644 --- a/kez-chat/web/src/routes/Messages.svelte +++ b/kez-chat/web/src/routes/Messages.svelte @@ -6,6 +6,7 @@ import { lookup, lookupByPrimary, ApiError } from "../lib/api.js"; import { inboxService } from "../lib/inbox-service.svelte.js"; import { verifySubject } from "../lib/verify.js"; + import { onboarding } from "../lib/onboarding.svelte.js"; import EmojiButton from "../lib/EmojiButton.svelte"; import Avatar from "../lib/Avatar.svelte"; import VerifiedBadge from "../lib/VerifiedBadge.svelte"; @@ -318,6 +319,13 @@ {/if} + + {#if !onboarding.onboarded} + + Finish setting up your account + + {/if} +
{ e.preventDefault(); startConversation(); }}> diff --git a/kez-chat/web/src/routes/Settings.svelte b/kez-chat/web/src/routes/Settings.svelte index 893b7be..17222ef 100644 --- a/kez-chat/web/src/routes/Settings.svelte +++ b/kez-chat/web/src/routes/Settings.svelte @@ -17,6 +17,12 @@ fireTestNotification, } from "../lib/inbox-service.svelte.js"; import { theme, type ThemeChoice } from "../lib/theme.svelte.js"; + import { onboarding } from "../lib/onboarding.svelte.js"; + + function openGettingStarted() { + onboarding.reopen(); + push("/welcome"); + } const themeOptions: { value: ThemeChoice; label: string }[] = [ { value: "light", label: "Light" }, @@ -199,6 +205,9 @@

Account

+ diff --git a/kez-chat/web/src/routes/Welcome.svelte b/kez-chat/web/src/routes/Welcome.svelte new file mode 100644 index 0000000..43829eb --- /dev/null +++ b/kez-chat/web/src/routes/Welcome.svelte @@ -0,0 +1,201 @@ + + +{#if session.unlocked} +
+
+
+

Welcome — let's get you set up

+

+ A couple of quick steps. You can skip and come back anytime from Settings. +

+
+ + {session.unlocked.handle}@{session.unlocked.server} +
+

{done} of {total} essentials done

+
+ +
    + +
  1. + +
    +

    Account created

    +

    Your identity is an Ed25519 keypair — no email, no password reset.

    +
    +
  2. + + +
  3. +
    + {onboarding.seedAcked ? "✓" : "🔑"} +
    +

    Back up your recovery seed

    +

    + This 32-byte seed is the only way to recover your + account. Lose it and it's gone forever — there's no reset. Write it + down offline. +

    + {#if !seedRevealed && !onboarding.seedAcked} + + {/if} + {#if seedRevealed} +
    +

    {seedHex}

    +
    + + {#if !onboarding.seedAcked} + + {/if} +
    +
    + {/if} + {#if onboarding.seedAcked} +

    ✓ Backed up

    + {/if} +
    +
    +
  4. + + +
  5. + {hasVerifiedProof ? "✓" : "✦"} +
    +

    Add your first proof

    +

    + Link a GitHub, a domain, or your nostr key to earn your verified + badge — so people know it's really you. +

    + {#if hasVerifiedProof} +

    ✓ You have a verified proof

    + {:else} + Add a proof + {/if} +
    +
  6. + + + {#if biometricAvailable || biometricEnrolled} +
  7. + {biometricEnrolled ? "✓" : "🔓"} +
    +

    Enable app lock · optional

    +

    Unlock with Touch ID / Face ID / Windows Hello instead of your passphrase.

    + {#if biometricEnrolled} +

    ✓ Enabled

    + {:else} + + {/if} +
    +
  8. + {/if} + + + {#if notifPerm !== "unsupported"} +
  9. + {notifPerm === "granted" ? "✓" : "🔔"} +
    +

    Turn on notifications · optional

    +

    Get pinged when a message arrives while you're away.

    + {#if notifPerm === "granted"} +

    ✓ Enabled

    + {:else if notifPerm === "denied"} +

    Blocked — re-enable in site settings.

    + {:else} + + {/if} +
    +
  10. + {/if} +
+ +
+ + +
+
+{/if}