mirror of
https://github.com/Such-Software/smirk-website.git
synced 2026-10-17 05:51:28 +02:00
docs: update TODO and tips page for alpha
- Simplify TODO with completed items - Tips page shows confirmation status
This commit is contained in:
@@ -1,138 +1,35 @@
|
|||||||
# Smirk Website TODO
|
# 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
|
## Completed
|
||||||
|
|
||||||
### Extension API (DONE - 2026-01-22)
|
- [x] **Tips Page** (2026-01-27)
|
||||||
- [x] Inject `window.smirk` API - Content script injects API object into pages
|
- Sent/received tabs with confirmation status
|
||||||
- [x] `window.smirk.connect()` - Returns public keys for all 5 assets
|
- Polling for confirmation updates
|
||||||
- [x] `window.smirk.signMessage(message)` - Signs message with user's chosen asset
|
- Claimable indicator badges
|
||||||
- [x] `window.smirk.isConnected()` - Check if site is connected
|
- [x] **Linked Accounts** (2026-01-26)
|
||||||
- [x] `window.smirk.disconnect()` - Revoke site access
|
- Settings page with platform linking
|
||||||
- [x] `window.smirk.getPublicKeys()` - Get public keys if already connected
|
- Telegram verification flow
|
||||||
- [x] Approval popup UI - Show user what site is requesting, allow approve/deny
|
- Unlink functionality
|
||||||
- [x] Origin allowlist - Track which origins user has approved
|
- [x] **Website Auth** (2026-01-25)
|
||||||
|
- Extension connection (`window.smirk`)
|
||||||
### Backend Auth Endpoints (DONE - 2026-01-24)
|
- Challenge-response with wallet signature
|
||||||
- [x] `POST /api/v1/auth/website/challenge` - Generate challenge with nonce + origin
|
- JWT session management
|
||||||
- [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.
|
|
||||||
|
|||||||
+132
-48
@@ -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 (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-medium rounded border bg-green-500/20 text-green-400 border-green-500/50">
|
||||||
|
Confirmed
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-medium rounded border bg-orange-500/20 text-orange-400 border-orange-500/50">
|
||||||
|
{confirmations}/{required} confirmations
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function TipsPage() {
|
export default function TipsPage() {
|
||||||
const [user, setUser] = useState<MeResponse | null>(null);
|
const [user, setUser] = useState<MeResponse | null>(null);
|
||||||
const [sentTips, setSentTips] = useState<SentTip[]>([]);
|
const [sentTips, setSentTips] = useState<SentTip[]>([]);
|
||||||
@@ -88,16 +116,24 @@ export default function TipsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all([getMe(token), getSentTips(token), getReceivedTips(token), getClaimableTips(token)])
|
const fetchData = () => {
|
||||||
.then(([userData, sentData, receivedData, claimableData]) => {
|
Promise.all([getMe(token), getSentTips(token), getReceivedTips(token), getClaimableTips(token)])
|
||||||
setUser(userData);
|
.then(([userData, sentData, receivedData, claimableData]) => {
|
||||||
setSentTips(sentData.tips);
|
setUser(userData);
|
||||||
setReceivedTips(receivedData.tips);
|
setSentTips(sentData.tips);
|
||||||
// Track which tips are claimable (pending, have encrypted_key)
|
setReceivedTips(receivedData.tips);
|
||||||
setClaimableTipIds(new Set(claimableData.tips.map((t: ClaimableTip) => t.id)));
|
// 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));
|
.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]);
|
}, [token]);
|
||||||
|
|
||||||
const handleOpenExtension = (action: 'claim' | 'clawback', tipId?: string) => {
|
const handleOpenExtension = (action: 'claim' | 'clawback', tipId?: string) => {
|
||||||
@@ -131,6 +167,10 @@ export default function TipsPage() {
|
|||||||
|
|
||||||
const pendingSent = sentTips.filter((t) => t.status === 'pending');
|
const pendingSent = sentTips.filter((t) => t.status === 'pending');
|
||||||
const pendingReceived = receivedTips.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 (
|
return (
|
||||||
<div className="min-h-screen p-8 max-w-3xl mx-auto">
|
<div className="min-h-screen p-8 max-w-3xl mx-auto">
|
||||||
@@ -188,7 +228,11 @@ export default function TipsPage() {
|
|||||||
>
|
>
|
||||||
Sent ({sentTips.length})
|
Sent ({sentTips.length})
|
||||||
{pendingSent.length > 0 && (
|
{pendingSent.length > 0 && (
|
||||||
<span className="ml-2 px-1.5 py-0.5 text-xs bg-yellow-500/30 text-yellow-400 rounded">
|
<span className={`ml-2 px-1.5 py-0.5 text-xs rounded ${
|
||||||
|
activeTab === 'sent'
|
||||||
|
? 'bg-yellow-700 text-white'
|
||||||
|
: 'bg-yellow-500/30 text-yellow-400'
|
||||||
|
}`}>
|
||||||
{pendingSent.length} pending
|
{pendingSent.length} pending
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -202,9 +246,22 @@ export default function TipsPage() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Received ({receivedTips.length})
|
Received ({receivedTips.length})
|
||||||
{pendingReceived.length > 0 && (
|
{claimableReceived.length > 0 && (
|
||||||
<span className="ml-2 px-1.5 py-0.5 text-xs bg-green-500/30 text-green-400 rounded">
|
<span className={`ml-2 px-1.5 py-0.5 text-xs rounded ${
|
||||||
{pendingReceived.length} claimable
|
activeTab === 'received'
|
||||||
|
? 'bg-green-700 text-white'
|
||||||
|
: 'bg-green-500/30 text-green-400'
|
||||||
|
}`}>
|
||||||
|
{claimableReceived.length} claimable
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{confirmingReceived.length > 0 && (
|
||||||
|
<span className={`ml-2 px-1.5 py-0.5 text-xs rounded ${
|
||||||
|
activeTab === 'received'
|
||||||
|
? 'bg-orange-700 text-white'
|
||||||
|
: 'bg-orange-500/30 text-orange-400'
|
||||||
|
}`}>
|
||||||
|
{confirmingReceived.length} confirming
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -224,7 +281,9 @@ export default function TipsPage() {
|
|||||||
sentTips.map((tip) => (
|
sentTips.map((tip) => (
|
||||||
<div
|
<div
|
||||||
key={tip.id}
|
key={tip.id}
|
||||||
className="bg-zinc-900 rounded-xl p-4 flex items-center justify-between"
|
className={`bg-zinc-900 rounded-xl p-4 flex items-center justify-between ${
|
||||||
|
tip.status === 'pending' && !tip.is_claimable ? 'border border-dashed border-zinc-700' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-1">
|
<div className="flex items-center gap-3 mb-1">
|
||||||
@@ -232,6 +291,13 @@ export default function TipsPage() {
|
|||||||
{formatAmount(tip.amount, tip.asset)}
|
{formatAmount(tip.amount, tip.asset)}
|
||||||
</span>
|
</span>
|
||||||
<StatusBadge status={tip.status} />
|
<StatusBadge status={tip.status} />
|
||||||
|
{tip.status === 'pending' && (
|
||||||
|
<ConfirmationBadge
|
||||||
|
confirmations={tip.funding_confirmations}
|
||||||
|
required={tip.confirmations_required}
|
||||||
|
isClaimable={tip.is_claimable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-zinc-500">
|
<p className="text-sm text-zinc-500">
|
||||||
To{' '}
|
To{' '}
|
||||||
@@ -281,40 +347,58 @@ export default function TipsPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
receivedTips.map((tip) => (
|
receivedTips.map((tip) => {
|
||||||
<div
|
const canClaim = tip.status === 'pending' && tip.is_claimable && claimableTipIds.has(tip.id);
|
||||||
key={tip.id}
|
const isConfirming = tip.status === 'pending' && !tip.is_claimable && tip.confirmations_required > 0;
|
||||||
className="bg-zinc-900 rounded-xl p-4 flex items-center justify-between"
|
|
||||||
>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-3 mb-1">
|
|
||||||
<span className={`font-mono font-medium ${tip.status === 'claimed' ? 'text-green-400' : 'text-white'}`}>
|
|
||||||
+{formatAmount(tip.amount, tip.asset)}
|
|
||||||
</span>
|
|
||||||
<StatusBadge status={tip.status} />
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-zinc-500">
|
|
||||||
{tip.recipient_platform && (
|
|
||||||
<span className="text-zinc-400">
|
|
||||||
Via {tip.recipient_platform}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="text-zinc-600 ml-2">
|
|
||||||
{formatDate(tip.created_at)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{tip.status === 'pending' && claimableTipIds.has(tip.id) && hasExtension && (
|
return (
|
||||||
<button
|
<div
|
||||||
onClick={() => handleOpenExtension('claim', tip.id)}
|
key={tip.id}
|
||||||
className="px-4 py-1.5 text-sm bg-[#fbeb0a] text-black font-medium rounded-lg hover:bg-[#d4c708] transition-colors"
|
className={`bg-zinc-900 rounded-xl p-4 flex items-center justify-between ${
|
||||||
>
|
isConfirming ? 'border border-dashed border-zinc-700' : ''
|
||||||
Claim
|
}`}
|
||||||
</button>
|
>
|
||||||
)}
|
<div className="flex-1">
|
||||||
</div>
|
<div className="flex items-center gap-3 mb-1">
|
||||||
))
|
<span className={`font-mono font-medium ${tip.status === 'claimed' ? 'text-green-400' : 'text-white'}`}>
|
||||||
|
+{formatAmount(tip.amount, tip.asset)}
|
||||||
|
</span>
|
||||||
|
<StatusBadge status={tip.status} />
|
||||||
|
{tip.status === 'pending' && (
|
||||||
|
<ConfirmationBadge
|
||||||
|
confirmations={tip.funding_confirmations}
|
||||||
|
required={tip.confirmations_required}
|
||||||
|
isClaimable={tip.is_claimable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-zinc-500">
|
||||||
|
{tip.recipient_platform && (
|
||||||
|
<span className="text-zinc-400">
|
||||||
|
Via {tip.recipient_platform}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="text-zinc-600 ml-2">
|
||||||
|
{formatDate(tip.created_at)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canClaim && hasExtension ? (
|
||||||
|
<button
|
||||||
|
onClick={() => handleOpenExtension('claim', tip.id)}
|
||||||
|
className="px-4 py-1.5 text-sm bg-[#fbeb0a] text-black font-medium rounded-lg hover:bg-[#d4c708] transition-colors"
|
||||||
|
>
|
||||||
|
Claim
|
||||||
|
</button>
|
||||||
|
) : isConfirming ? (
|
||||||
|
<span className="px-4 py-1.5 text-sm text-orange-400 font-medium">
|
||||||
|
Confirming...
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -185,6 +185,12 @@ export interface SentTip {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
claimed_at: string | null;
|
claimed_at: string | null;
|
||||||
clawed_back_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 {
|
export interface ClaimableTip {
|
||||||
@@ -207,6 +213,12 @@ export interface ReceivedTip {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
claimed_at: string | null;
|
claimed_at: string | null;
|
||||||
clawed_back_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[] }> {
|
export async function getSentTips(token: string): Promise<{ tips: SentTip[] }> {
|
||||||
|
|||||||
Reference in New Issue
Block a user