diff --git a/src/components/OnboardingGate.tsx b/src/components/OnboardingGate.tsx
index 97a1731a..31bb9b39 100644
--- a/src/components/OnboardingGate.tsx
+++ b/src/components/OnboardingGate.tsx
@@ -1,5 +1,6 @@
import {
useCallback,
+ useEffect,
useMemo,
useState,
type ReactNode,
@@ -29,7 +30,9 @@ import {
type OrgProfileDraft,
} from '@/components/onboarding/VerifierIdentityStep';
import { VerifierBioStep } from '@/components/onboarding/VerifierBioStep';
+import { VerifierStatementEditor } from '@/components/organizations/VerifierStatementEditor';
import { usePublishOrgProfile } from '@/hooks/usePublishOrgProfile';
+import { useVerifierStatement } from '@/hooks/useVerifierStatement';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useCurrentUser } from '@/hooks/useCurrentUser';
@@ -184,6 +187,15 @@ function CaptiveOverlay() {
const { mutateAsync: publishOrgProfile, isPending: isPublishingOrg } =
usePublishOrgProfile();
+ // Tracks whether the verifier statement (kind 14672) has been published,
+ // gating the statement step's Next button. Seeded from any existing
+ // statement so a returning verifier isn't blocked.
+ const { isVerifier } = useVerifierStatement(user?.pubkey);
+ const [statementPublished, setStatementPublished] = useState(false);
+ useEffect(() => {
+ if (isVerifier) setStatementPublished(true);
+ }, [isVerifier]);
+
// Linear progress bar position. Once the user has chosen the verifier
// role, the bar tracks the extended verifier step list so the four
// sub-flow screens are reflected; otherwise the base three-step list is
@@ -364,8 +376,14 @@ function CaptiveOverlay() {
);
case 'orgStatement':
// Verifier sub-flow step 3 — publish the verifier statement
- // (kind 14672). Filled in by a later commit.
- return ;
+ // (kind 14672), reusing the shared editor.
+ return (
+
+ );
case 'orgVerifyHowto':
// Verifier sub-flow step 4 — teach the verify gesture, then finish.
// Filled in by a later commit.
@@ -481,10 +499,8 @@ function RoleStep({ role, onPick }: RoleStepProps) {
}
/**
- * Placeholder shell for the four verifier sub-flow steps. Each real step
- * (organization identity, bio, statement, how-to-verify) replaces this in a
- * later commit; for now it just renders a Continue button so the state
- * machine and navigation can be exercised end-to-end.
+ * Placeholder shell for verifier sub-flow steps not yet built out. Renders a
+ * single Continue button so the state machine can be exercised end-to-end.
*/
function VerifierStepShell({ onContinue }: { onContinue: () => void }) {
return (
@@ -496,6 +512,48 @@ function VerifierStepShell({ onContinue }: { onContinue: () => void }) {
);
}
+interface VerifierStatementStepProps {
+ published: boolean;
+ onPublishedChange: (published: boolean) => void;
+ onContinue: () => void;
+}
+
+/**
+ * Verifier sub-flow step 3 — publish the verifier statement (kind 14672).
+ * Wraps the shared {@link VerifierStatementEditor} with the captive-flow
+ * heading and a Continue button that unlocks once a statement is live.
+ */
+function VerifierStatementStep({
+ published,
+ onPublishedChange,
+ onContinue,
+}: VerifierStatementStepProps) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t('onboarding.verifier.statement.title')}
+
+
+ {t('onboarding.verifier.statement.subtitle')}
+
+
+
+
+
+
+
+ );
+}
+
interface RoleCardProps {
icon: ReactNode;
title: string;
diff --git a/src/components/organizations/VerifierStatementEditor.tsx b/src/components/organizations/VerifierStatementEditor.tsx
new file mode 100644
index 00000000..bd84ca3b
--- /dev/null
+++ b/src/components/organizations/VerifierStatementEditor.tsx
@@ -0,0 +1,154 @@
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Loader2 } from 'lucide-react';
+
+import { MilkdownEditor } from '@/components/markdown/MilkdownEditor';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent } from '@/components/ui/card';
+import { useCurrentUser } from '@/hooks/useCurrentUser';
+import { useToast } from '@/hooks/useToast';
+import {
+ useSetVerifierStatement,
+ useVerifierStatement,
+} from '@/hooks/useVerifierStatement';
+import { cn } from '@/lib/utils';
+
+interface VerifierStatementEditorProps {
+ className?: string;
+ /**
+ * Called after a non-empty statement has been published or updated, and
+ * with `false` after a withdrawal. Lets hosts (e.g. the captive onboarding
+ * flow) react to publish state — for instance, enabling a "Next" button.
+ */
+ onPublishedChange?: (isPublished: boolean) => void;
+}
+
+/**
+ * The functional verifier-statement editor (kind 14672): a WYSIWYG Markdown
+ * surface with publish / update / withdraw controls and a live hydrate from
+ * the user's existing statement.
+ *
+ * Extracted from OrganizationsPage so both the public /organizations tool
+ * and the captive verifier onboarding flow render the exact same editor and
+ * stay in sync. Assumes a logged-in user; callers gate on auth.
+ */
+export function VerifierStatementEditor({
+ className,
+ onPublishedChange,
+}: VerifierStatementEditorProps) {
+ const { t } = useTranslation();
+ const { user } = useCurrentUser();
+ const { toast } = useToast();
+
+ const { statement, isLoading } = useVerifierStatement(user?.pubkey);
+ const { mutateAsync: setStatement, isPending } = useSetVerifierStatement();
+
+ const [value, setValue] = useState('');
+ const [hydrated, setHydrated] = useState(false);
+
+ useEffect(() => {
+ if (!hydrated && !isLoading) {
+ setValue(statement ?? '');
+ setHydrated(true);
+ }
+ }, [hydrated, isLoading, statement]);
+
+ const trimmed = value.trim();
+ const isPublished = !!statement;
+ const unchanged = trimmed === (statement ?? '');
+
+ const handlePublish = async () => {
+ try {
+ await setStatement(trimmed);
+ toast({
+ title: trimmed
+ ? t('verifier.publishedToast')
+ : t('verifier.withdrawnToast'),
+ });
+ onPublishedChange?.(!!trimmed);
+ } catch (error) {
+ toast({
+ title: t('verifier.errorToast'),
+ description: error instanceof Error ? error.message : String(error),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ const handleWithdraw = async () => {
+ try {
+ await setStatement('');
+ setValue('');
+ toast({ title: t('verifier.withdrawnToast') });
+ onPublishedChange?.(false);
+ } catch (error) {
+ toast({
+ title: t('verifier.errorToast'),
+ description: error instanceof Error ? error.message : String(error),
+ variant: 'destructive',
+ });
+ }
+ };
+
+ return (
+
+
+ {/* Prompt */}
+
+
{t('verifier.promptLabel')}
+
+ {t('verifier.prompt')}
+
+
+
+ {isLoading && !hydrated ? (
+
+
+ {t('verifier.loading')}
+
+ ) : (
+ <>
+ {/* WYSIWYG markdown editor: formatting toolbar + rich-text
+ editing surface, value flows back out as markdown. */}
+
+
+
+
+
+
+
+ {isPublished && (
+
+ )}
+
+
+
+ {t('verifier.disclaimer')}
+
+ >
+ )}
+
+
+ );
+}
+
+export default VerifierStatementEditor;
diff --git a/src/pages/OrganizationsPage.tsx b/src/pages/OrganizationsPage.tsx
index 5f7c6863..c79c949a 100644
--- a/src/pages/OrganizationsPage.tsx
+++ b/src/pages/OrganizationsPage.tsx
@@ -1,27 +1,21 @@
import { useSeoMeta } from '@unhead/react';
-import { useEffect, useState } from 'react';
+import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ArrowRight,
BadgeCheck,
Building2,
CircleCheck,
- Loader2,
ShieldCheck,
} from 'lucide-react';
-import { MilkdownEditor } from '@/components/markdown/MilkdownEditor';
import { LoginArea } from '@/components/auth/LoginArea';
import { VerifyTutorial } from '@/components/organizations/VerifyTutorial';
-import { Button } from '@/components/ui/button';
+import { VerifierStatementEditor } from '@/components/organizations/VerifierStatementEditor';
import { Card, CardContent } from '@/components/ui/card';
import { useAppContext } from '@/hooks/useAppContext';
import { useCurrentUser } from '@/hooks/useCurrentUser';
-import { useToast } from '@/hooks/useToast';
-import {
- useSetVerifierStatement,
- useVerifierStatement,
-} from '@/hooks/useVerifierStatement';
+import { useVerifierStatement } from '@/hooks/useVerifierStatement';
/**
* The /organizations page. A landing-style document modeled on the
@@ -189,20 +183,11 @@ export function OrganizationsPage() {
function VerifierEditor() {
const { t } = useTranslation();
const { user } = useCurrentUser();
- const { toast } = useToast();
- const { statement, isLoading } = useVerifierStatement(user?.pubkey);
- const { mutateAsync: setStatement, isPending } = useSetVerifierStatement();
-
- const [value, setValue] = useState('');
- const [hydrated, setHydrated] = useState(false);
-
- useEffect(() => {
- if (!hydrated && !isLoading) {
- setValue(statement ?? '');
- setHydrated(true);
- }
- }, [hydrated, isLoading, statement]);
+ const { isVerifier } = useVerifierStatement(user?.pubkey);
+ // Track publish state locally so the tutorial appears/disappears
+ // immediately on publish / withdraw, seeded from the queried state.
+ const [isPublished, setIsPublished] = useState(isVerifier);
// Logged out: instruct the visitor to log in with — or create — their
// organization's Nostr profile before they can publish a statement.
@@ -227,106 +212,13 @@ function VerifierEditor() {
);
}
- const trimmed = value.trim();
- const isPublished = !!statement;
- const unchanged = trimmed === (statement ?? '');
-
- const handlePublish = async () => {
- try {
- await setStatement(trimmed);
- toast({
- title: trimmed
- ? t('verifier.publishedToast')
- : t('verifier.withdrawnToast'),
- });
- } catch (error) {
- toast({
- title: t('verifier.errorToast'),
- description: error instanceof Error ? error.message : String(error),
- variant: 'destructive',
- });
- }
- };
-
- const handleWithdraw = async () => {
- try {
- await setStatement('');
- setValue('');
- toast({ title: t('verifier.withdrawnToast') });
- } catch (error) {
- toast({
- title: t('verifier.errorToast'),
- description: error instanceof Error ? error.message : String(error),
- variant: 'destructive',
- });
- }
- };
-
return (
-
-
- {/* Prompt */}
-
-
- {t('verifier.promptLabel')}
-
-
- {t('verifier.prompt')}
-
-
-
- {isLoading && !hydrated ? (
-
-
- {t('verifier.loading')}
-
- ) : (
- <>
- {/* WYSIWYG markdown editor: formatting toolbar + rich-text
- editing surface, value flows back out as markdown. */}
-
-
-
-
-
-
-
- {isPublished && (
-
- )}
-
-
-
- {t('verifier.disclaimer')}
-
- >
- )}
-
-
+
{/* Once the org's statement is live, teach them the actual
verify gesture: the three-dots menu on any campaign card. */}
- {isPublished && }
+ {(isPublished || isVerifier) && }