diff --git a/src/components/OnboardingGate.tsx b/src/components/OnboardingGate.tsx index 67be077f..146c29de 100644 --- a/src/components/OnboardingGate.tsx +++ b/src/components/OnboardingGate.tsx @@ -1,6 +1,5 @@ import { useCallback, - useEffect, useMemo, useState, type ReactNode, @@ -33,7 +32,7 @@ import { VerifierBioStep } from '@/components/onboarding/VerifierBioStep'; import { VerifierStatementEditor } from '@/components/organizations/VerifierStatementEditor'; import { VerifyTutorial } from '@/components/organizations/VerifyTutorial'; import { usePublishOrgProfile } from '@/hooks/usePublishOrgProfile'; -import { useVerifierStatement } from '@/hooks/useVerifierStatement'; +import { useSetVerifierStatement } from '@/hooks/useVerifierStatement'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -188,15 +187,6 @@ 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 @@ -380,8 +370,6 @@ function CaptiveOverlay() { // (kind 14672), reusing the shared editor. return ( ); @@ -535,22 +523,58 @@ function VerifierHowtoStep({ onFinish }: { onFinish: () => 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. + * + * One header and one combined subtext sit above a borderless + * {@link VerifierStatementEditor}. There's no separate publish button: the + * primary button publishes the statement (when there's content) and then + * advances. A returning verifier can withdraw inline. */ function VerifierStatementStep({ - published, - onPublishedChange, onContinue, }: VerifierStatementStepProps) { const { t } = useTranslation(); + const { toast } = useToast(); + const { mutateAsync: setStatement, isPending } = useSetVerifierStatement(); + + const [value, setValue] = useState(''); + const [alreadyPublished, setAlreadyPublished] = useState(false); + + const trimmed = value.trim(); + + const handleContinue = useCallback(async () => { + try { + await setStatement(trimmed); + toast({ title: t('verifier.publishedToast') }); + onContinue(); + } catch (error) { + toast({ + title: t('verifier.errorToast'), + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); + } + }, [setStatement, trimmed, toast, t, onContinue]); + + const handleWithdraw = useCallback(async () => { + try { + await setStatement(''); + setValue(''); + setAlreadyPublished(false); + toast({ title: t('verifier.withdrawnToast') }); + } catch (error) { + toast({ + title: t('verifier.errorToast'), + description: error instanceof Error ? error.message : String(error), + variant: 'destructive', + }); + } + }, [setStatement, toast, t]); + return (
@@ -562,15 +586,25 @@ function VerifierStatementStep({

- + setAlreadyPublished(!!s)} + showWithdraw={alreadyPublished} + onWithdraw={handleWithdraw} + isWithdrawing={isPending} + />
); diff --git a/src/components/organizations/VerifierStatementEditor.tsx b/src/components/organizations/VerifierStatementEditor.tsx index bd84ca3b..7f1572a5 100644 --- a/src/components/organizations/VerifierStatementEditor.tsx +++ b/src/components/organizations/VerifierStatementEditor.tsx @@ -4,150 +4,87 @@ 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 { useVerifierStatement } from '@/hooks/useVerifierStatement'; import { cn } from '@/lib/utils'; interface VerifierStatementEditorProps { + /** Current markdown value (controlled). */ + value: string; + onChange: (value: string) => void; + /** Hydration callback — fired once with the user's existing statement. */ + onHydrated?: (statement: string) => void; + /** Show a Withdraw control (only when a statement is already published). */ + showWithdraw?: boolean; + onWithdraw?: () => void; + isWithdrawing?: boolean; 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. + * The verifier-statement (kind 14672) markdown editing surface. * - * 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. + * A controlled, borderless WYSIWYG editor: the host owns the value and the + * publish action (publishing is wired to the onboarding step's primary + * button). The editor only renders the editing surface, hydrating once from + * the user's existing statement, plus an optional inline Withdraw control. */ export function VerifierStatementEditor({ + value, + onChange, + onHydrated, + showWithdraw = false, + onWithdraw, + isWithdrawing = false, 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 ?? ''); + onChange(statement ?? ''); + onHydrated?.(statement ?? ''); setHydrated(true); } - }, [hydrated, isLoading, statement]); + }, [hydrated, isLoading, statement, onChange, onHydrated]); - 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', - }); - } - }; + if (isLoading && !hydrated) { + return ( +
+ + {t('verifier.loading')} +
+ ); + } return ( - - - {/* Prompt */} -
-

{t('verifier.promptLabel')}

-

- {t('verifier.prompt')} -

-
+
+ {/* Borderless WYSIWYG markdown editor — no surrounding card/box so it + blends into the step. */} +
+ +
- {isLoading && !hydrated ? ( -
- - {t('verifier.loading')} -
- ) : ( - <> - {/* WYSIWYG markdown editor: formatting toolbar + rich-text - editing surface, value flows back out as markdown. */} -
- -
- -
- - - {isPublished && ( - - )} -
- -

- {t('verifier.disclaimer')} -

- - )} - - + {showWithdraw && onWithdraw && ( + + )} +
); } diff --git a/src/locales/en.json b/src/locales/en.json index 54fc5a1b..2885d527 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -171,7 +171,7 @@ "publishFailedDescription": "Your account is ready, but the organization profile couldn't be published. You can finish it later from settings.", "statement": { "title": "Publish your verifier statement", - "subtitle": "Explain how your organization checks out campaigns before vouching for them. This is public and helps donors trust your badge." + "subtitle": "Explain how your organization checks out campaigns before vouching for them — the checks you run, the evidence you require, how you confirm an organizer's identity. This is published publicly and shown on your profile so donors can trust your badge." }, "howto": { "title": "How to verify a campaign", @@ -1590,8 +1590,13 @@ }, "getStarted": { "eyebrow": "Get started", - "title": "Publish your statement", - "lede": "Sign in with your organization's profile to publish, update, or withdraw your verification statement." + "title": "Become a verifier", + "lede": "Set up your organization and publish your verification statement in a few quick steps." + }, + "getStartedCard": { + "title": "Set up your organization", + "body": "We'll walk you through creating your organization's profile, publishing your verification statement, and verifying your first campaign.", + "cta": "Start verifying" }, "loginGateTitle": "Sign in with your organization's profile", "loginGateBody": "Log in with your organization's Nostr profile, or create one, to get started. Once you're signed in, you can publish your verification statement here.", diff --git a/src/pages/OrganizationsPage.tsx b/src/pages/OrganizationsPage.tsx index c79c949a..c47a26b4 100644 --- a/src/pages/OrganizationsPage.tsx +++ b/src/pages/OrganizationsPage.tsx @@ -1,5 +1,4 @@ import { useSeoMeta } from '@unhead/react'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ArrowRight, @@ -9,12 +8,12 @@ import { ShieldCheck, } from 'lucide-react'; -import { LoginArea } from '@/components/auth/LoginArea'; import { VerifyTutorial } from '@/components/organizations/VerifyTutorial'; -import { VerifierStatementEditor } from '@/components/organizations/VerifierStatementEditor'; +import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { useAppContext } from '@/hooks/useAppContext'; import { useCurrentUser } from '@/hooks/useCurrentUser'; +import { useOnboarding } from '@/contexts/onboardingContextDef'; import { useVerifierStatement } from '@/hooks/useVerifierStatement'; /** @@ -183,16 +182,11 @@ export function OrganizationsPage() { function VerifierEditor() { const { t } = useTranslation(); const { user } = useCurrentUser(); - 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); + const { startSignup } = useOnboarding(); - // Logged out: instruct the visitor to log in with — or create — their - // organization's Nostr profile before they can publish a statement. - if (!user) { - return ( + return ( +
@@ -200,25 +194,26 @@ function VerifierEditor() {

- {t('organizations.loginGateTitle')} + {t('organizations.getStartedCard.title')}

- {t('organizations.loginGateBody')} + {t('organizations.getStartedCard.body')}

- +
- ); - } - - return ( -
- {/* Once the org's statement is live, teach them the actual verify gesture: the three-dots menu on any campaign card. */} - {(isPublished || isVerifier) && } + {isVerifier && }
); }