diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index c647fec..eb008ec 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -10,6 +10,8 @@ import { unlinkSocial, getMyUsername, setUsername, + getDiscordAuthUrl, + completeDiscordOAuth, type MeResponse, type LinkedSocial, } from '@/lib/api'; @@ -35,8 +37,44 @@ export default function SettingsPage() { const [linkError, setLinkError] = useState(null); const [polling, setPolling] = useState(false); + // Discord linking state + const [discordLinking, setDiscordLinking] = useState(false); + const [discordError, setDiscordError] = useState(null); + const token = typeof window !== 'undefined' ? localStorage.getItem('smirk_token') : null; + // Handle Discord OAuth callback (code + state in URL params) + useEffect(() => { + if (typeof window === 'undefined' || !token) return; + + const params = new URLSearchParams(window.location.search); + const code = params.get('code'); + const state = params.get('state'); + + if (code && state) { + // Clean URL immediately + window.history.replaceState({}, '', '/settings'); + + // Complete Discord OAuth + setDiscordLinking(true); + setDiscordError(null); + + completeDiscordOAuth(token, code, state) + .then((linkedSocial) => { + // Refresh socials list + return getLinkedSocials(token).then((data) => { + setSocials(data.socials); + }); + }) + .catch((err) => { + setDiscordError(err instanceof Error ? err.message : 'Failed to link Discord'); + }) + .finally(() => { + setDiscordLinking(false); + }); + } + }, [token]); + useEffect(() => { if (!token) { setLoading(false); @@ -144,6 +182,21 @@ export default function SettingsPage() { setPolling(false); }; + const handleLinkDiscord = async () => { + if (!token) return; + setDiscordLinking(true); + setDiscordError(null); + + try { + const { auth_url } = await getDiscordAuthUrl(token); + // Redirect to Discord OAuth + window.location.href = auth_url; + } catch (err) { + setDiscordError(err instanceof Error ? err.message : 'Failed to start Discord link'); + setDiscordLinking(false); + } + }; + if (loading) { return (
@@ -164,6 +217,7 @@ export default function SettingsPage() { } const telegramSocial = socials.find((s) => s.platform === 'telegram'); + const discordSocial = socials.find((s) => s.platform === 'discord'); return (
@@ -352,9 +406,57 @@ export default function SettingsPage() { )}
+ {/* Discord */} +
+
+
+
+ + + +
+
+

Discord

+ {discordSocial?.verified ? ( +

+ {discordSocial.display_name || discordSocial.username || 'Linked'} +

+ ) : discordLinking ? ( +

Linking...

+ ) : ( +

Not linked

+ )} +
+
+ + {discordSocial?.verified ? ( + + ) : ( + + )} +
+ + {discordError && ( +
+

{discordError}

+
+ )} +
+ {/* Future platforms */}
- {['Discord', 'Signal', 'Matrix'].map((platform) => ( + {['Signal', 'Matrix'].map((platform) => (
diff --git a/src/lib/api.ts b/src/lib/api.ts index 7c9ef9d..7a5e808 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -294,6 +294,49 @@ export async function getClaimableTips(token: string): Promise<{ tips: Claimable return res.json(); } +// ============================================================================= +// Discord OAuth2 Linking +// ============================================================================= + +export interface DiscordAuthUrlResponse { + auth_url: string; +} + +export async function getDiscordAuthUrl(token: string): Promise { + const res = await fetch(`${API_URL}/api/v1/socials/discord/auth-url`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Failed to get Discord auth URL'); + } + + return res.json(); +} + +export async function completeDiscordOAuth( + token: string, + code: string, + state: string +): Promise { + const res = await fetch(`${API_URL}/api/v1/socials/discord/callback`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ code, state }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({})); + throw new Error(data.error || 'Failed to link Discord account'); + } + + return res.json(); +} + // ============================================================================= // Username Management // =============================================================================