mirror of
https://github.com/Such-Software/smirk-website.git
synced 2026-10-17 05:51:28 +02:00
feat: add settings page with Telegram account linking
This commit is contained in:
@@ -1,84 +1,99 @@
|
||||
# Smirk Website TODO
|
||||
|
||||
## Phase 1: Foundation (Critical Path)
|
||||
See [smirk-backend/docs/SOCIAL_TIPPING.md](../smirk-backend/docs/SOCIAL_TIPPING.md) for full architecture.
|
||||
|
||||
## Completed
|
||||
|
||||
### Extension API (DONE - 2026-01-22)
|
||||
- [x] **Inject `window.smirk` API** - Content script injects API object into pages
|
||||
- [x] **`window.smirk.connect()`** - Returns public keys for all 5 assets
|
||||
- [x] **`window.smirk.signMessage(message)`** - Signs message with all 5 asset keys
|
||||
- [x] **`window.smirk.isConnected()`** - Check if site is connected
|
||||
- [x] **`window.smirk.disconnect()`** - Revoke site access
|
||||
- [x] **`window.smirk.getPublicKeys()`** - Get public keys if already connected
|
||||
- [x] **Approval popup UI** - Show user what site is requesting, allow approve/deny
|
||||
- [x] **Origin allowlist** - Track which origins user has approved (persisted to storage)
|
||||
- [x] Inject `window.smirk` API - Content script injects API object into pages
|
||||
- [x] `window.smirk.connect()` - Returns public keys for all 5 assets
|
||||
- [x] `window.smirk.signMessage(message)` - Signs message with user's chosen asset
|
||||
- [x] `window.smirk.isConnected()` - Check if site is connected
|
||||
- [x] `window.smirk.disconnect()` - Revoke site access
|
||||
- [x] `window.smirk.getPublicKeys()` - Get public keys if already connected
|
||||
- [x] Approval popup UI - Show user what site is requesting, allow approve/deny
|
||||
- [x] Origin allowlist - Track which origins user has approved
|
||||
|
||||
### Backend Auth Endpoints (DONE - 2026-01-24)
|
||||
- [x] `POST /api/v1/auth/website/challenge` - Generate challenge with nonce + origin
|
||||
- [x] `POST /api/v1/auth/website/verify` - Verify signatures, issue JWT session token
|
||||
- [x] `POST /api/v1/auth/website/verify` - Verify signature, issue JWT session token
|
||||
- [x] `GET /api/v1/auth/me` - Get current user info from JWT
|
||||
- [ ] JWT middleware for protected routes (frontend side)
|
||||
- [x] Single-asset authentication - User picks favorite coin to sign with
|
||||
|
||||
### Website Basics
|
||||
- [ ] Initialize Next.js project with App Router
|
||||
- [ ] Tailwind CSS setup
|
||||
- [ ] Basic layout (header, footer)
|
||||
- [ ] Landing page with "Connect with Smirk" button
|
||||
- [ ] Extension detection (`window.smirk` check)
|
||||
- [ ] "Install Smirk" fallback if extension not detected
|
||||
### Website Foundation (DONE - 2026-01-25)
|
||||
- [x] Next.js 16 project with App Router
|
||||
- [x] Tailwind CSS setup
|
||||
- [x] Landing page with connect button
|
||||
- [x] Extension detection (`window.smirk` check)
|
||||
- [x] Authentication flow (connect → choose coin → sign → logged in)
|
||||
- [x] JWT storage in localStorage
|
||||
- [x] Logout functionality
|
||||
|
||||
### Linked Accounts (DONE - 2026-01-26)
|
||||
- [x] Settings page at `/settings`
|
||||
- [x] List currently linked platforms
|
||||
- [x] "Link Telegram" button → verification flow
|
||||
- [x] Verification UI: enter username → get code → send to bot → confirm
|
||||
- [x] Unlink platform button
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Core Dashboard
|
||||
|
||||
### Authentication Flow
|
||||
- [ ] Connect button triggers `window.smirk.connect()`
|
||||
- [ ] Challenge generation and signing
|
||||
- [ ] Backend verification and JWT storage
|
||||
- [ ] Protected route middleware
|
||||
- [ ] Logout functionality
|
||||
|
||||
### Dashboard - Basic
|
||||
### Dashboard Home
|
||||
- [ ] Display user UUID
|
||||
- [ ] Display balances for all 5 assets
|
||||
- [ ] Auto-refresh balances
|
||||
- [ ] Loading states and error handling
|
||||
- [ ] Display balances for all 5 assets (fetched via backend)
|
||||
- [ ] Auto-refresh balances (polling or WebSocket)
|
||||
- [ ] Loading skeletons and error states
|
||||
|
||||
### Dashboard - Tip History
|
||||
- [ ] List of tips sent
|
||||
### Tip History (Basic)
|
||||
- [ ] List of tips sent (existing tip_links)
|
||||
- [ ] List of tips received
|
||||
- [ ] Pagination
|
||||
- [ ] Filter by asset type
|
||||
- [ ] Click to view tip details
|
||||
|
||||
## Phase 3: Account Management
|
||||
## Phase 3: Social Tipping Dashboard
|
||||
|
||||
### Connected Accounts
|
||||
- [ ] Link Telegram account
|
||||
- [ ] Link Discord account
|
||||
- [ ] Link other platforms (Twitter/X, etc.)
|
||||
- [ ] Manage/unlink accounts
|
||||
- [ ] Display linked accounts on dashboard
|
||||
### Tips Sent (`/tips/sent`)
|
||||
- [ ] List social tips with status (pending/claimed/clawed-back)
|
||||
- [ ] Filter by platform (Telegram/Discord/direct)
|
||||
- [ ] Clawback button (enabled anytime for unclaimed tips)
|
||||
- [ ] Confirmation modal for clawback
|
||||
- [ ] "Resend notification" option
|
||||
|
||||
### Settings
|
||||
- [ ] Notification preferences
|
||||
- [ ] Privacy settings
|
||||
- [ ] Export account data
|
||||
### Tips Received (`/tips/received`)
|
||||
- [ ] List tips received (claimed and pending)
|
||||
- [ ] Claim pending tips (verify platform ownership first)
|
||||
- [ ] Link to transaction on blockchain explorer
|
||||
|
||||
## Phase 4: Social Features (Future)
|
||||
### Public Tips (`/tips/public`) - Phase 2
|
||||
- [ ] View public tips sent by user
|
||||
- [ ] Claim status tracking
|
||||
- [ ] Share link/payload copy button
|
||||
|
||||
## Phase 5: Additional Platforms
|
||||
|
||||
- [ ] Discord linking flow
|
||||
- [ ] Signal linking flow (future)
|
||||
- [ ] Simplex linking flow (future)
|
||||
- [ ] Matrix linking flow (future)
|
||||
|
||||
## Phase 6: Social Features (Future)
|
||||
|
||||
### Direct Tipping
|
||||
- [ ] Send tip to another Smirk user by UUID
|
||||
- [ ] Send tip via username/handle
|
||||
- [ ] Send tip via username/handle (from website)
|
||||
- [ ] Tip confirmation flow
|
||||
- [ ] Tip notifications
|
||||
|
||||
### Messaging (Future)
|
||||
- [ ] Direct messages between users
|
||||
- [ ] Encrypted messaging (E2E?)
|
||||
- [ ] Message notifications
|
||||
- [ ] Encrypted messaging (E2E)
|
||||
|
||||
### Discovery (Future)
|
||||
- [ ] Public profiles (opt-in)
|
||||
- [ ] Search users
|
||||
- [ ] Leaderboards?
|
||||
- [ ] Tipping leaderboards (opt-in)
|
||||
|
||||
## Technical Debt / Polish
|
||||
|
||||
@@ -104,41 +119,20 @@
|
||||
|
||||
## Notes
|
||||
|
||||
### Extension Work Required (add to smirk-extension/TODO.md)
|
||||
### Signing Algorithm Reference
|
||||
|
||||
The website cannot function without these extension features:
|
||||
|
||||
1. **Content script injection** - Must inject `window.smirk` on all pages (or configurable origins)
|
||||
2. **Message passing** - Content script ↔ Background script communication
|
||||
3. **Approval UI** - Popup for connect/sign requests
|
||||
4. **Security** - Origin tracking, user approval persistence
|
||||
|
||||
### Signing Algorithm Notes
|
||||
|
||||
For message signing (all implemented in extension):
|
||||
- **BTC/LTC**: ECDSA signature (secp256k1) with Bitcoin message signing format
|
||||
- **XMR/WOW**: Ed25519 signature using private spend key
|
||||
- **Grin**: Ed25519 signature using slatepack key
|
||||
|
||||
Backend must implement verification for all signature types.
|
||||
For message signing (implemented in extension, verified by backend):
|
||||
- **BTC/LTC**: ECDSA (secp256k1) with Bitcoin message signing format
|
||||
- **XMR/WOW**: Ed25519 using private spend key
|
||||
- **Grin**: Ed25519 using slatepack key
|
||||
|
||||
### Challenge Message Format
|
||||
|
||||
```
|
||||
Sign this message to authenticate with smirk.cash
|
||||
|
||||
Timestamp: 2026-01-22T12:00:00Z
|
||||
Smirk Authentication Challenge
|
||||
Nonce: abc123...
|
||||
Origin: https://smirk.cash
|
||||
Origin: https://smirk.to
|
||||
Timestamp: 1706000000
|
||||
```
|
||||
|
||||
Challenge should expire after ~5 minutes to prevent replay attacks.
|
||||
|
||||
### Single-Asset Authentication (DONE - 2026-01-25)
|
||||
|
||||
User picks their favorite coin to sign with:
|
||||
- Simpler UX - choose one coin, sign once
|
||||
- User data - "What's your favorite coin?" insight
|
||||
- Any of the 5 coins proves wallet ownership
|
||||
|
||||
This positions Smirk as infrastructure for wallet-based authentication across multiple cryptocurrencies.
|
||||
Challenge expires after 5 minutes.
|
||||
|
||||
+17
-7
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { getChallenge, verifySignature, type AuthResponse } from '@/lib/api';
|
||||
|
||||
type Asset = 'btc' | 'ltc' | 'xmr' | 'wow' | 'grin';
|
||||
@@ -220,13 +221,22 @@ export default function Home() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-6 py-2 border border-zinc-700 rounded-lg
|
||||
hover:border-zinc-500 transition-colors text-sm"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
<div className="flex gap-4">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="px-6 py-2 bg-[#fbeb0a] text-black font-medium rounded-lg
|
||||
hover:bg-[#d4c708] transition-colors text-sm"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-6 py-2 border border-zinc-700 rounded-lg
|
||||
hover:border-zinc-500 transition-colors text-sm"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
getMe,
|
||||
getLinkedSocials,
|
||||
registerSocial,
|
||||
unlinkSocial,
|
||||
type MeResponse,
|
||||
type LinkedSocial,
|
||||
} from '@/lib/api';
|
||||
|
||||
type LinkStep = 'idle' | 'entering' | 'verifying';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [user, setUser] = useState<MeResponse | null>(null);
|
||||
const [socials, setSocials] = useState<LinkedSocial[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Telegram linking state
|
||||
const [linkStep, setLinkStep] = useState<LinkStep>('idle');
|
||||
const [telegramUsername, setTelegramUsername] = useState('');
|
||||
const [verificationCode, setVerificationCode] = useState<string | null>(null);
|
||||
const [botLink, setBotLink] = useState<string | null>(null);
|
||||
const [linkError, setLinkError] = useState<string | null>(null);
|
||||
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('smirk_token') : null;
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([getMe(token), getLinkedSocials(token)])
|
||||
.then(([userData, socialsData]) => {
|
||||
setUser(userData);
|
||||
setSocials(socialsData.socials);
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [token]);
|
||||
|
||||
const handleStartLink = () => {
|
||||
setLinkStep('entering');
|
||||
setLinkError(null);
|
||||
};
|
||||
|
||||
const handleSubmitUsername = async () => {
|
||||
if (!token || !telegramUsername.trim()) return;
|
||||
|
||||
setLinkError(null);
|
||||
try {
|
||||
const result = await registerSocial(token, 'telegram', telegramUsername.trim());
|
||||
setVerificationCode(result.verification_code);
|
||||
setBotLink(result.bot_link);
|
||||
setLinkStep('verifying');
|
||||
} catch (err) {
|
||||
setLinkError(err instanceof Error ? err.message : 'Failed to start verification');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlink = async (platform: string) => {
|
||||
if (!token) return;
|
||||
if (!confirm(`Unlink ${platform}?`)) return;
|
||||
|
||||
try {
|
||||
await unlinkSocial(token, platform);
|
||||
setSocials(socials.filter((s) => s.platform !== platform));
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to unlink');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelLink = () => {
|
||||
setLinkStep('idle');
|
||||
setTelegramUsername('');
|
||||
setVerificationCode(null);
|
||||
setBotLink(null);
|
||||
setLinkError(null);
|
||||
};
|
||||
|
||||
const handleVerificationDone = async () => {
|
||||
if (!token) return;
|
||||
// Refresh socials to check if verified
|
||||
try {
|
||||
const socialsData = await getLinkedSocials(token);
|
||||
setSocials(socialsData.socials);
|
||||
const telegram = socialsData.socials.find((s) => s.platform === 'telegram');
|
||||
if (telegram?.verified) {
|
||||
handleCancelLink();
|
||||
} else {
|
||||
setLinkError('Verification not complete. Send the code to the bot and try again.');
|
||||
}
|
||||
} catch {
|
||||
setLinkError('Failed to check verification status');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!token || !user) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
<p className="text-zinc-400 mb-4">Please sign in first</p>
|
||||
<Link href="/" className="text-[#fbeb0a] hover:underline">
|
||||
Go to home
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const telegramSocial = socials.find((s) => s.platform === 'telegram');
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-8 max-w-2xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<Link href="/" className="flex items-center gap-3">
|
||||
<Image src="/logo.svg" alt="Smirk" width={40} height={40} />
|
||||
<span className="creepster-text text-xl">SMIRK</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold mb-8">Settings</h1>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-900/20 border border-red-500/50 rounded-lg">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Info */}
|
||||
<section className="mb-8">
|
||||
<h2 className="text-lg font-semibold mb-4 text-zinc-400">Account</h2>
|
||||
<div className="bg-zinc-900 rounded-xl p-4">
|
||||
<p className="text-zinc-500 text-xs mb-1">User ID</p>
|
||||
<p className="font-mono text-sm break-all">{user.id}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Linked Accounts */}
|
||||
<section>
|
||||
<h2 className="text-lg font-semibold mb-4 text-zinc-400">Linked Accounts</h2>
|
||||
|
||||
{/* Telegram */}
|
||||
<div className="bg-zinc-900 rounded-xl p-4 mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-[#0088cc] rounded-full flex items-center justify-center">
|
||||
<svg className="w-6 h-6 text-white" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.63-.2-1.12-.31-1.08-.66.02-.18.27-.36.74-.55 2.92-1.27 4.86-2.11 5.83-2.51 2.78-1.16 3.35-1.36 3.73-1.36.08 0 .27.02.39.12.1.08.13.19.14.27-.01.06.01.24 0 .38z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Telegram</p>
|
||||
{telegramSocial?.verified ? (
|
||||
<p className="text-sm text-green-400">@{telegramSocial.username}</p>
|
||||
) : telegramSocial?.pending_verification ? (
|
||||
<p className="text-sm text-yellow-400">Pending: @{telegramSocial.username}</p>
|
||||
) : (
|
||||
<p className="text-sm text-zinc-500">Not linked</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{telegramSocial?.verified ? (
|
||||
<button
|
||||
onClick={() => handleUnlink('telegram')}
|
||||
className="px-3 py-1 text-sm text-red-400 border border-red-400/50 rounded hover:bg-red-400/10"
|
||||
>
|
||||
Unlink
|
||||
</button>
|
||||
) : linkStep === 'idle' ? (
|
||||
<button
|
||||
onClick={handleStartLink}
|
||||
className="px-4 py-2 bg-[#fbeb0a] text-black font-medium rounded-lg hover:bg-[#d4c708]"
|
||||
>
|
||||
Link
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Link flow UI */}
|
||||
{linkStep === 'entering' && (
|
||||
<div className="mt-4 pt-4 border-t border-zinc-800">
|
||||
<label className="block text-sm text-zinc-400 mb-2">
|
||||
Enter your Telegram username
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={telegramUsername}
|
||||
onChange={(e) => setTelegramUsername(e.target.value)}
|
||||
placeholder="@username"
|
||||
className="flex-1 px-3 py-2 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-[#fbeb0a]"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSubmitUsername}
|
||||
disabled={!telegramUsername.trim()}
|
||||
className="px-4 py-2 bg-[#fbeb0a] text-black font-medium rounded-lg hover:bg-[#d4c708] disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelLink}
|
||||
className="px-4 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{linkError && <p className="mt-2 text-sm text-red-400">{linkError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{linkStep === 'verifying' && verificationCode && (
|
||||
<div className="mt-4 pt-4 border-t border-zinc-800">
|
||||
<div className="bg-zinc-800 rounded-lg p-4 mb-4">
|
||||
<p className="text-sm text-zinc-400 mb-2">Your verification code:</p>
|
||||
<p className="text-2xl font-mono font-bold text-[#fbeb0a]">{verificationCode}</p>
|
||||
</div>
|
||||
|
||||
<ol className="text-sm text-zinc-400 space-y-2 mb-4">
|
||||
<li>
|
||||
1. Open{' '}
|
||||
<a
|
||||
href={botLink || 'https://t.me/smirk_wallet_bot'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#0088cc] hover:underline"
|
||||
>
|
||||
@smirk_wallet_bot
|
||||
</a>{' '}
|
||||
on Telegram
|
||||
</li>
|
||||
<li>2. Send: <code className="bg-zinc-700 px-2 py-0.5 rounded">/verify {verificationCode}</code></li>
|
||||
<li>3. Click "I've verified" below</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleVerificationDone}
|
||||
className="px-4 py-2 bg-[#fbeb0a] text-black font-medium rounded-lg hover:bg-[#d4c708]"
|
||||
>
|
||||
I've verified
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCancelLink}
|
||||
className="px-4 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{linkError && <p className="mt-2 text-sm text-red-400">{linkError}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Future platforms */}
|
||||
<div className="space-y-3">
|
||||
{['Discord', 'Signal', 'Matrix'].map((platform) => (
|
||||
<div key={platform} className="bg-zinc-900/50 rounded-xl p-4 opacity-50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-zinc-700 rounded-full" />
|
||||
<div>
|
||||
<p className="font-medium">{platform}</p>
|
||||
<p className="text-sm text-zinc-500">Coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -73,3 +73,72 @@ export async function getMe(token: string): Promise<MeResponse> {
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Social Account Linking
|
||||
// =============================================================================
|
||||
|
||||
export interface RegisterSocialResponse {
|
||||
verification_code: string;
|
||||
expires_at: string;
|
||||
bot_link: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
export interface LinkedSocial {
|
||||
platform: string;
|
||||
username: string;
|
||||
verified: boolean;
|
||||
verified_at: string | null;
|
||||
pending_verification: boolean;
|
||||
}
|
||||
|
||||
export interface LinkedSocialsResponse {
|
||||
socials: LinkedSocial[];
|
||||
}
|
||||
|
||||
export async function registerSocial(
|
||||
token: string,
|
||||
platform: string,
|
||||
username: string
|
||||
): Promise<RegisterSocialResponse> {
|
||||
const res = await fetch(`${API_URL}/api/v1/socials/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ platform, username }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Failed to start verification');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getLinkedSocials(token: string): Promise<LinkedSocialsResponse> {
|
||||
const res = await fetch(`${API_URL}/api/v1/socials/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to get linked accounts');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function unlinkSocial(token: string, platform: string): Promise<void> {
|
||||
const res = await fetch(`${API_URL}/api/v1/socials/${platform}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Failed to unlink account');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user