From 69cfa3bdda189ac9bdfb67efd6d37e884043260c Mon Sep 17 00:00:00 2001 From: snackman Date: Tue, 5 May 2026 13:09:25 -0400 Subject: [PATCH 1/3] feat: require X account verification to comment - Add x_verified, x_oauth_id, x_avatar_url columns to profiles - Create useXVerification hook for OAuth identity linking - Gate comment input behind X verification with connect CTA - Replace free-text X handle input with OAuth verify/disconnect UI - Re-enable CommentSection with verification gate - Add analytics tracking for X connect/disconnect --- src/components/AuthModal.tsx | 38 +++++-- src/components/CommentSection.tsx | 81 ++++++++------ src/components/EventCard.tsx | 3 +- src/hooks/useEventComments.ts | 12 +- src/hooks/useProfile.ts | 7 +- src/hooks/useXVerification.ts | 105 ++++++++++++++++++ src/lib/analytics.ts | 5 + src/lib/profile-cache.ts | 4 +- src/lib/types.ts | 4 + .../20260505_add_x_verification.sql | 5 + 10 files changed, 211 insertions(+), 53 deletions(-) create mode 100644 src/hooks/useXVerification.ts create mode 100644 supabase/migrations/20260505_add_x_verification.sql diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index 1f299125..671f5636 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -14,6 +14,7 @@ import { getSocialLinks } from '@/lib/social-urls'; import { supabase } from '@/lib/supabase'; import { useProfile } from '@/hooks/useProfile'; +import { useXVerification } from '@/hooks/useXVerification'; import { useFriends } from '@/hooks/useFriends'; import { useFriendRequests } from '@/hooks/useFriendRequests'; import type { ETHDenverEvent, NativeAd, UserSearchResult, FriendRequest } from '@/lib/types'; @@ -377,6 +378,7 @@ export function UserMenu({ events, itinerary, onOpenFriends, onSubmitEvent, pend const { friendCount, refreshFriends: localRefreshFriends } = useFriends(); const { config } = useAdminConfig(); const { theme, setTheme } = useTheme(); + const { isXVerified, linkX, unlinkX } = useXVerification(); const refreshFriends = useCallback(async () => { await localRefreshFriends(); @@ -765,18 +767,32 @@ export function UserMenu({ events, itinerary, onOpenFriends, onSubmitEvent, pend -
-
- {'\u{1D54F}'} - setXHandle(e.target.value.replace(/^@/, ''))} - placeholder="X handle" - className="flex-1 bg-transparent text-[var(--theme-text-primary)] text-sm px-2 py-2 focus:outline-none placeholder:text-[var(--theme-text-muted)]" - /> + {/* X Account */} + {profile?.x_verified ? ( +
+ + @{profile.x_handle} + +
-
+ ) : ( + + )}
diff --git a/src/components/CommentSection.tsx b/src/components/CommentSection.tsx index fb3dc61d..3987f5f7 100644 --- a/src/components/CommentSection.tsx +++ b/src/components/CommentSection.tsx @@ -10,6 +10,7 @@ import { getDisplayName } from '@/lib/user-display'; import UserAvatar from './UserAvatar'; import ProfileCardModal from './ProfileCardModal'; import { trackCommentExpand, trackCommentAdd, trackCommentDelete, trackCommentVisibilityToggle } from '@/lib/analytics'; +import { useXVerification } from '@/hooks/useXVerification'; interface CommentSectionProps { eventId: string; @@ -19,6 +20,7 @@ interface CommentSectionProps { export function CommentSection({ eventId, commentCount = 0, eventName }: CommentSectionProps) { const { user } = useAuth(); + const { isXVerified, linkX } = useXVerification(); const [expanded, setExpanded] = useState(false); const [isMobile, setIsMobile] = useState(false); const { comments, loading, addComment, deleteComment } = useEventComments( @@ -164,45 +166,60 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment ); // Shared input content - const inputContent = user && ( -
-
- setText(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }} - placeholder="Add a comment..." - maxLength={500} - className="flex-1 bg-transparent text-xs text-[var(--theme-text-primary)] placeholder-slate-500 outline-none min-w-0" - /> + const inputContent = user ? ( + isXVerified ? ( +
+
+ setText(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }} + placeholder="Add a comment..." + maxLength={500} + className="flex-1 bg-transparent text-xs text-[var(--theme-text-primary)] placeholder-slate-500 outline-none min-w-0" + /> + +
+ ) : ( -
- ); + ) + ) : null; // Mobile: render bottom-sheet modal via portal if (isMobile) { diff --git a/src/components/EventCard.tsx b/src/components/EventCard.tsx index 1e4c2e9d..05d317b7 100644 --- a/src/components/EventCard.tsx +++ b/src/components/EventCard.tsx @@ -404,8 +404,7 @@ export const EventCard = memo(function EventCard({ compact={compact} /> )} - {/* Comments disabled until social verification is in place */} - {/* */} + {checkedInFriends && checkedInFriends.length > 0 && ( ) - ) : null; + ) : ( + + ); // Mobile: render bottom-sheet modal via portal if (isMobile) { @@ -274,11 +286,9 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment
{/* Sticky input at bottom */} - {user && ( -
- {inputContent} -
- )} +
+ {inputContent} +
, document.body @@ -297,6 +307,7 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment telegramHandle={selectedProfile.telegramHandle} /> )} + setShowAuth(false)} /> ); } @@ -338,6 +349,7 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment telegramHandle={selectedProfile.telegramHandle} /> )} + setShowAuth(false)} />
); } From 0abe8c7dd81292a56d1ffeb64c495ce722e14c1d Mon Sep 17 00:00:00 2001 From: snackman Date: Tue, 5 May 2026 23:39:34 -0400 Subject: [PATCH 3/3] fix: unified "Connect X to comment" CTA with inline email prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Always show "Connect X to comment" regardless of auth state - If not logged in, clicking shows email input → creates account silently (no OTP required until next login) → proceeds to X OAuth - If logged in but X not verified → goes straight to X OAuth - Removes separate "Sign in" button and AuthModal dependency Co-Authored-By: Claude Opus 4.6 --- src/components/CommentSection.tsx | 166 +++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 51 deletions(-) diff --git a/src/components/CommentSection.tsx b/src/components/CommentSection.tsx index 6472fd11..ac38ced1 100644 --- a/src/components/CommentSection.tsx +++ b/src/components/CommentSection.tsx @@ -5,13 +5,13 @@ import { createPortal } from 'react-dom'; import { Send, Trash2, MessageCircle, X } from 'lucide-react'; import { useAuth } from '@/contexts/AuthContext'; import { useEventComments } from '@/hooks/useEventComments'; +import { supabase } from '@/lib/supabase'; import { timeAgo } from '@/lib/time-parse'; import { getDisplayName } from '@/lib/user-display'; import UserAvatar from './UserAvatar'; import ProfileCardModal from './ProfileCardModal'; import { trackCommentExpand, trackCommentAdd, trackCommentDelete, trackCommentVisibilityToggle } from '@/lib/analytics'; import { useXVerification } from '@/hooks/useXVerification'; -import { AuthModal } from './AuthModal'; interface CommentSectionProps { eventId: string; @@ -29,7 +29,10 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment ); const [text, setText] = useState(''); const [visibility, setVisibility] = useState<'public' | 'friends'>('public'); - const [showAuth, setShowAuth] = useState(false); + const [showEmailPrompt, setShowEmailPrompt] = useState(false); + const [email, setEmail] = useState(''); + const [emailLoading, setEmailLoading] = useState(false); + const [emailError, setEmailError] = useState(null); const [selectedProfile, setSelectedProfile] = useState<{ userId: string; displayName?: string | null; @@ -167,69 +170,131 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment ); + // Handle email sign-up then X OAuth link + const handleEmailThenLinkX = async () => { + if (!email.trim()) return; + setEmailLoading(true); + setEmailError(null); + + // Sign up with a random password — auto-confirms since enable_confirmations = false + // Next login they'll use OTP + const { error } = await supabase.auth.signUp({ + email: email.trim(), + password: crypto.randomUUID(), + }); + + if (error) { + // If account exists, they need to sign in properly + if (error.message?.includes('already registered') || error.status === 422) { + setEmailError('Account exists — sign in from the menu first'); + } else { + setEmailError(error.message); + } + setEmailLoading(false); + return; + } + + // Account created + session active, now start X OAuth + setEmailLoading(false); + linkX(); + }; + // Shared input content - const inputContent = user ? ( - isXVerified ? ( -
-
- setText(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSubmit(); - } - }} - placeholder="Add a comment..." - maxLength={500} - className="flex-1 bg-transparent text-xs text-[var(--theme-text-primary)] placeholder-slate-500 outline-none min-w-0" - /> - -
+ const inputContent = user && isXVerified ? ( +
+
+ setText(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }} + placeholder="Add a comment..." + maxLength={500} + className="flex-1 bg-transparent text-xs text-[var(--theme-text-primary)] placeholder-slate-500 outline-none min-w-0" + />
- ) : ( - ) +
+ ) : user && !isXVerified ? ( + // Logged in but X not verified — go straight to X OAuth + + ) : showEmailPrompt ? ( + // Not logged in — email prompt before X OAuth +
+
+ setEmail(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleEmailThenLinkX(); + } + }} + placeholder="Enter your email..." + className="flex-1 bg-[var(--theme-bg-tertiary)]/50 border border-[var(--theme-border-primary)] rounded-lg px-3 py-2 text-sm text-[var(--theme-text-primary)] placeholder-slate-500 outline-none min-w-0" + autoFocus + /> + +
+ {emailError && ( +

{emailError}

+ )} +
) : ( + // Not logged in — show connect CTA ); @@ -307,7 +372,6 @@ export function CommentSection({ eventId, commentCount = 0, eventName }: Comment telegramHandle={selectedProfile.telegramHandle} /> )} - setShowAuth(false)} /> ); }