diff --git a/TODO.md b/TODO.md index a468ce7..743dc57 100644 --- a/TODO.md +++ b/TODO.md @@ -1,138 +1,35 @@ # Smirk Website TODO -See [smirk-backend/docs/SOCIAL_TIPPING.md](../smirk-backend/docs/SOCIAL_TIPPING.md) for full architecture. +## Phase 2: Dashboard + +- [ ] Display balances for all 5 assets +- [ ] Display user UUID +- [ ] Auto-refresh balances + +## Phase 3: Additional Platforms + +- [ ] Discord linking flow +- [ ] Signal / Matrix / Simplex (future) + +## Lower Priority + +- [ ] Dark/light theme toggle +- [ ] Mobile responsive improvements +- [ ] E2E tests with Playwright + +--- ## 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 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 signature, issue JWT session token -- [x] `GET /api/v1/auth/me` - Get current user info from JWT -- [x] Single-asset authentication - User picks favorite coin to sign with - -### 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 - -### Dashboard Home -- [ ] Display user UUID -- [ ] Display balances for all 5 assets (fetched via backend) -- [ ] Auto-refresh balances (polling or WebSocket) -- [ ] Loading skeletons and error states - -### 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: Social Tipping 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 - -### Tips Received (`/tips/received`) -- [ ] List tips received (claimed and pending) -- [ ] Claim pending tips (verify platform ownership first) -- [ ] Link to transaction on blockchain explorer - -### 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 (from website) -- [ ] Tip confirmation flow - -### Messaging (Future) -- [ ] Direct messages between users -- [ ] Encrypted messaging (E2E) - -### Discovery (Future) -- [ ] Public profiles (opt-in) -- [ ] Search users -- [ ] Tipping leaderboards (opt-in) - -## Technical Debt / Polish - -- [ ] Comprehensive error handling -- [ ] Loading skeletons -- [ ] Mobile responsive design -- [ ] Dark/light theme (sync with extension setting?) -- [ ] Accessibility (a11y) -- [ ] SEO optimization -- [ ] Analytics integration -- [ ] Rate limiting on frontend -- [ ] E2E tests with Playwright - -## Deployment - -- [ ] Docker configuration -- [ ] Nginx reverse proxy setup (alongside backend) -- [ ] Environment configuration -- [ ] CI/CD pipeline -- [ ] Monitoring and alerting - ---- - -## Notes - -### Signing Algorithm Reference - -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 - -``` -Smirk Authentication Challenge -Nonce: abc123... -Origin: https://smirk.to -Timestamp: 1706000000 -``` - -Challenge expires after 5 minutes. +- [x] **Tips Page** (2026-01-27) + - Sent/received tabs with confirmation status + - Polling for confirmation updates + - Claimable indicator badges +- [x] **Linked Accounts** (2026-01-26) + - Settings page with platform linking + - Telegram verification flow + - Unlink functionality +- [x] **Website Auth** (2026-01-25) + - Extension connection (`window.smirk`) + - Challenge-response with wallet signature + - JWT session management diff --git a/src/app/tips/page.tsx b/src/app/tips/page.tsx index 796c68e..d1526ac 100644 --- a/src/app/tips/page.tsx +++ b/src/app/tips/page.tsx @@ -56,6 +56,34 @@ function StatusBadge({ status }: { status: string }) { ); } +function ConfirmationBadge({ + confirmations, + required, + isClaimable +}: { + confirmations: number; + required: number; + isClaimable: boolean; +}) { + if (required === 0) { + return null; // BTC/LTC don't need confirmations + } + + if (isClaimable) { + return ( + + Confirmed + + ); + } + + return ( + + {confirmations}/{required} confirmations + + ); +} + export default function TipsPage() { const [user, setUser] = useState(null); const [sentTips, setSentTips] = useState([]); @@ -88,16 +116,24 @@ export default function TipsPage() { return; } - Promise.all([getMe(token), getSentTips(token), getReceivedTips(token), getClaimableTips(token)]) - .then(([userData, sentData, receivedData, claimableData]) => { - setUser(userData); - setSentTips(sentData.tips); - setReceivedTips(receivedData.tips); - // Track which tips are claimable (pending, have encrypted_key) - setClaimableTipIds(new Set(claimableData.tips.map((t: ClaimableTip) => t.id))); - }) - .catch((err) => setError(err.message)) - .finally(() => setLoading(false)); + const fetchData = () => { + Promise.all([getMe(token), getSentTips(token), getReceivedTips(token), getClaimableTips(token)]) + .then(([userData, sentData, receivedData, claimableData]) => { + setUser(userData); + setSentTips(sentData.tips); + setReceivedTips(receivedData.tips); + // Track which tips are claimable (pending, confirmed, have encrypted_key) + setClaimableTipIds(new Set(claimableData.tips.map((t: ClaimableTip) => t.id))); + }) + .catch((err) => setError(err.message)) + .finally(() => setLoading(false)); + }; + + fetchData(); + + // Poll for updates every 30 seconds for confirmation status + const interval = setInterval(fetchData, 30000); + return () => clearInterval(interval); }, [token]); const handleOpenExtension = (action: 'claim' | 'clawback', tipId?: string) => { @@ -131,6 +167,10 @@ export default function TipsPage() { const pendingSent = sentTips.filter((t) => t.status === 'pending'); const pendingReceived = receivedTips.filter((t) => t.status === 'pending'); + // Count actually claimable (confirmed) pending tips + const claimableReceived = pendingReceived.filter((t) => t.is_claimable); + // Count tips waiting for confirmations + const confirmingReceived = pendingReceived.filter((t) => !t.is_claimable && t.confirmations_required > 0); return (
@@ -188,7 +228,11 @@ export default function TipsPage() { > Sent ({sentTips.length}) {pendingSent.length > 0 && ( - + {pendingSent.length} pending )} @@ -202,9 +246,22 @@ export default function TipsPage() { }`} > Received ({receivedTips.length}) - {pendingReceived.length > 0 && ( - - {pendingReceived.length} claimable + {claimableReceived.length > 0 && ( + + {claimableReceived.length} claimable + + )} + {confirmingReceived.length > 0 && ( + + {confirmingReceived.length} confirming )} @@ -224,7 +281,9 @@ export default function TipsPage() { sentTips.map((tip) => (
@@ -232,6 +291,13 @@ export default function TipsPage() { {formatAmount(tip.amount, tip.asset)} + {tip.status === 'pending' && ( + + )}

To{' '} @@ -281,40 +347,58 @@ export default function TipsPage() {

) : ( - receivedTips.map((tip) => ( -
-
-
- - +{formatAmount(tip.amount, tip.asset)} - - -
-

- {tip.recipient_platform && ( - - Via {tip.recipient_platform} - - )} - - {formatDate(tip.created_at)} - -

-
+ receivedTips.map((tip) => { + const canClaim = tip.status === 'pending' && tip.is_claimable && claimableTipIds.has(tip.id); + const isConfirming = tip.status === 'pending' && !tip.is_claimable && tip.confirmations_required > 0; - {tip.status === 'pending' && claimableTipIds.has(tip.id) && hasExtension && ( - - )} -
- )) + return ( +
+
+
+ + +{formatAmount(tip.amount, tip.asset)} + + + {tip.status === 'pending' && ( + + )} +
+

+ {tip.recipient_platform && ( + + Via {tip.recipient_platform} + + )} + + {formatDate(tip.created_at)} + +

+
+ + {canClaim && hasExtension ? ( + + ) : isConfirming ? ( + + Confirming... + + ) : null} +
+ ); + }) )}
)} diff --git a/src/lib/api.ts b/src/lib/api.ts index aed2feb..efb8072 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -185,6 +185,12 @@ export interface SentTip { created_at: string; claimed_at: string | null; clawed_back_at: string | null; + /** Current number of confirmations for funding tx */ + funding_confirmations: number; + /** Required confirmations before tip is claimable (XMR/GRIN=10, WOW=4, BTC/LTC=0) */ + confirmations_required: number; + /** Whether the tip has enough confirmations to be claimed */ + is_claimable: boolean; } export interface ClaimableTip { @@ -207,6 +213,12 @@ export interface ReceivedTip { created_at: string; claimed_at: string | null; clawed_back_at: string | null; + /** Current number of confirmations for funding tx */ + funding_confirmations: number; + /** Required confirmations before tip is claimable (XMR/GRIN=10, WOW=4, BTC/LTC=0) */ + confirmations_required: number; + /** Whether the tip has enough confirmations to be claimed */ + is_claimable: boolean; } export async function getSentTips(token: string): Promise<{ tips: SentTip[] }> {