feat: add public tip claiming UI

- Extract fragment key from URL
- Check confirmations before allowing claim
- Add claim button with loading/error states
- Update SmirkAPI types for claimPublicTip
This commit is contained in:
jwinterm
2026-01-27 10:50:42 -05:00
parent 570b48b464
commit 8581d3ad95
3 changed files with 132 additions and 2 deletions
+123 -2
View File
@@ -29,6 +29,10 @@ export default function TipPage() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hasExtension, setHasExtension] = useState<boolean | null>(null);
const [fragmentKey, setFragmentKey] = useState<string | null>(null);
const [claiming, setClaiming] = useState(false);
const [claimError, setClaimError] = useState<string | null>(null);
const [claimSuccess, setClaimSuccess] = useState<{ txid: string } | null>(null);
useEffect(() => {
// Check for extension
@@ -38,6 +42,14 @@ export default function TipPage() {
return () => clearTimeout(timeout);
}, []);
// Extract fragment key from URL
useEffect(() => {
const hash = window.location.hash;
if (hash && hash.length > 1) {
setFragmentKey(hash.slice(1)); // Remove the # prefix
}
}, []);
useEffect(() => {
if (!tipId) return;
@@ -57,6 +69,44 @@ export default function TipPage() {
loadTip();
}, [tipId]);
const handleClaim = async () => {
if (!window.smirk || !fragmentKey || !tipId) return;
setClaiming(true);
setClaimError(null);
try {
// First connect to extension (if not already)
await window.smirk.connect();
// Claim the tip
const result = await window.smirk.claimPublicTip(tipId, fragmentKey);
if (result.success && result.txid) {
setClaimSuccess({ txid: result.txid });
// Refresh tip info to show claimed status
const tipInfo = await getPublicTipInfo(tipId);
setTip(tipInfo);
} else {
setClaimError(result.error || 'Failed to claim tip');
}
} catch (err) {
setClaimError(err instanceof Error ? err.message : 'Failed to claim tip');
} finally {
setClaiming(false);
}
};
const isClaimable = tip?.status === 'pending' &&
tip.is_public &&
fragmentKey &&
(tip.funding_confirmations ?? 0) >= (tip.confirmations_required ?? 0);
const needsConfirmations = tip?.status === 'pending' &&
tip.is_public &&
fragmentKey &&
(tip.funding_confirmations ?? 0) < (tip.confirmations_required ?? 0);
const coin = tip ? COINS[tip.asset.toLowerCase()] : null;
return (
@@ -150,8 +200,79 @@ export default function TipPage() {
</div>
</div>
{/* Claim instructions */}
{tip.status === 'pending' && (
{/* Claim success */}
{claimSuccess && (
<div className="bg-green-500/10 border border-green-500/50 rounded-xl p-4">
<h3 className="text-green-400 font-semibold mb-2">Claimed!</h3>
<p className="text-zinc-400 text-sm">
Funds are being sent to your wallet.
</p>
<p className="text-zinc-500 text-xs mt-2 font-mono break-all">
TX: {claimSuccess.txid}
</p>
</div>
)}
{/* Needs confirmations */}
{needsConfirmations && !claimSuccess && (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-[#fbeb0a]">Waiting for Confirmations</h2>
<div className="text-zinc-400 text-sm">
<p>This tip is still confirming on the blockchain.</p>
<p className="mt-2">
<span className="text-[#fbeb0a] font-semibold">{tip?.funding_confirmations ?? 0}</span>
{' / '}
<span>{tip?.confirmations_required ?? 0}</span>
{' confirmations'}
</p>
<p className="mt-3 text-zinc-500 text-xs">
Refresh this page to check progress.
</p>
</div>
</div>
)}
{/* Claim button for public tips */}
{isClaimable && !claimSuccess && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">
{hasExtension ? 'Ready to Claim!' : 'Install Smirk Wallet'}
</h2>
{hasExtension ? (
<div className="space-y-4">
<button
onClick={handleClaim}
disabled={claiming}
className="px-8 py-4 bg-[#fbeb0a] text-black font-bold rounded-xl
hover:bg-[#d4c708] transition-colors disabled:opacity-50
disabled:cursor-not-allowed text-lg w-full"
>
{claiming ? 'Claiming...' : 'Claim This Tip'}
</button>
{claimError && (
<p className="text-red-400 text-sm">{claimError}</p>
)}
</div>
) : (
<div className="text-zinc-400 text-sm space-y-4">
<p>You need the Smirk Wallet extension to claim tips.</p>
<a
href="https://chrome.google.com/webstore/detail/smirk-wallet"
target="_blank"
rel="noopener noreferrer"
className="inline-block px-6 py-3 bg-[#fbeb0a] text-black font-bold rounded-xl
hover:bg-[#d4c708] transition-colors"
>
Install Smirk Wallet
</a>
</div>
)}
</div>
)}
{/* Targeted tip instructions (no fragment key) */}
{tip.status === 'pending' && !fragmentKey && !tip.is_public && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">
{hasExtension ? 'Claim This Tip' : 'Install Smirk Wallet'}
+2
View File
@@ -154,6 +154,8 @@ export interface PublicTipInfo {
status: string;
created_at: string;
is_public: boolean;
funding_confirmations: number;
confirmations_required: number;
}
export async function getPublicTipInfo(tipId: string): Promise<PublicTipInfo> {
+7
View File
@@ -19,6 +19,12 @@ interface SmirkSignResult {
signatures: SmirkSignature[];
}
interface SmirkClaimResult {
success: boolean;
txid?: string;
error?: string;
}
interface SmirkAPI {
isSmirk: boolean;
connect(): Promise<SmirkPublicKeys>;
@@ -26,6 +32,7 @@ interface SmirkAPI {
isConnected(): Promise<boolean>;
getPublicKeys(): Promise<SmirkPublicKeys | null>;
signMessage(message: string): Promise<SmirkSignResult>;
claimPublicTip(tipId: string, fragmentKey: string): Promise<SmirkClaimResult>;
}
interface Window {