fix chrome link and add OG

This commit is contained in:
jwinterm
2026-04-10 11:00:21 -04:00
parent daff4002ff
commit 9b9a2d2d7f
7 changed files with 471 additions and 337 deletions
+4
View File
@@ -40,3 +40,7 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# repo-to-text
.repo-to-text-settings.yaml
repo-to-text_*.txt
+28 -2
View File
@@ -7,9 +7,35 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
const siteUrl = "https://smirk.cash";
export const metadata: Metadata = {
title: "Smirk Wallet",
description: "Sign in with your Smirk wallet",
title: {
default: "Smirk Wallet",
template: "%s | Smirk Wallet",
},
description:
"Multi-coin crypto wallet browser extension. Send, receive, and tip BTC, LTC, XMR, WOW, and GRIN.",
metadataBase: new URL(siteUrl),
openGraph: {
title: "Smirk Wallet",
description:
"Multi-coin crypto wallet browser extension. Send, receive, and tip BTC, LTC, XMR, WOW, and GRIN.",
url: siteUrl,
siteName: "Smirk Wallet",
type: "website",
locale: "en_US",
},
twitter: {
card: "summary_large_image",
title: "Smirk Wallet",
description:
"Multi-coin crypto wallet browser extension. Send, receive, and tip BTC, LTC, XMR, WOW, and GRIN.",
},
icons: {
icon: "/logo.svg",
apple: "/logo.svg",
},
};
export default function RootLayout({
+57
View File
@@ -0,0 +1,57 @@
import { ImageResponse } from "next/og";
export const runtime = "edge";
export const alt = "Smirk Wallet";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image() {
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "#0a0a0a",
gap: "24px",
}}
>
{/* Smirk emoji as placeholder for logo */}
<div style={{ fontSize: 120, display: "flex" }}>😏</div>
<div
style={{
fontSize: 64,
fontWeight: 700,
color: "#fbeb0a",
display: "flex",
}}
>
Smirk Wallet
</div>
<div
style={{
fontSize: 28,
color: "#a1a1aa",
display: "flex",
}}
>
BTC • LTC • XMR • WOW • GRIN
</div>
<div
style={{
fontSize: 22,
color: "#71717a",
display: "flex",
}}
>
Multi-coin crypto wallet & tipping extension
</div>
</div>
),
{ ...size }
);
}
+1 -1
View File
@@ -202,7 +202,7 @@ export default function Home() {
<p className="creepster-text text-xl text-[#fbeb0a]">GET SMIRKED</p>
<div className="flex gap-4">
<a
href="https://chrome.google.com/webstore/detail/smirk-wallet"
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg
+335
View File
@@ -0,0 +1,335 @@
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import Link from 'next/link';
import { getPublicTipInfo, type PublicTipInfo } from '@/lib/api';
const COINS: Record<string, { name: string; icon: string; divisor: number; decimals: number }> = {
btc: { name: 'Bitcoin', icon: '/coins/bitcoin.svg', divisor: 100_000_000, decimals: 8 },
ltc: { name: 'Litecoin', icon: '/coins/litecoin.svg', divisor: 100_000_000, decimals: 8 },
xmr: { name: 'Monero', icon: '/coins/monero.svg', divisor: 1_000_000_000_000, decimals: 12 },
wow: { name: 'Wownero', icon: '/coins/wownero.svg', divisor: 100_000_000_000, decimals: 11 },
grin: { name: 'Grin', icon: '/coins/grin.svg', divisor: 1_000_000_000, decimals: 9 },
};
export function formatAmount(amount: number, asset: string): string {
const coin = COINS[asset.toLowerCase()];
if (!coin) return `${amount} ${asset.toUpperCase()}`;
const value = amount / coin.divisor;
return `${value.toFixed(Math.min(coin.decimals, 8))} ${asset.toUpperCase()}`;
}
export default function TipPageClient({ tipId }: { tipId: string }) {
const [tip, setTip] = useState<PublicTipInfo | null>(null);
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
const check = () => setHasExtension(!!window.smirk);
check();
const timeout = setTimeout(check, 500);
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;
async function loadTip() {
try {
setLoading(true);
setError(null);
const tipInfo = await getPublicTipInfo(tipId);
setTip(tipInfo);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load tip');
} finally {
setLoading(false);
}
}
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 (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
{/* Logo */}
<div className="mb-8 flex flex-col items-center">
<Link href="/" className="flex flex-col items-center">
<h1 className="creepster-text text-3xl mb-2">SMIRK WALLET</h1>
<div className="logo-glow">
<Image
src="/logo.svg"
alt="Smirk"
width={100}
height={100}
priority
/>
</div>
</Link>
</div>
{/* Loading state */}
{loading && (
<div className="flex flex-col items-center gap-4">
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
<p className="text-zinc-400">Loading tip...</p>
</div>
)}
{/* Error state */}
{error && !loading && (
<div className="flex flex-col items-center gap-6 text-center">
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center">
<span className="text-3xl">!</span>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Tip Not Found</h2>
<p className="text-zinc-400 text-sm max-w-md">
{error}
</p>
</div>
<Link
href="/"
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
>
Go Home
</Link>
</div>
)}
{/* Tip found */}
{tip && !loading && (
<div className="flex flex-col items-center gap-6 text-center max-w-md">
{/* Status indicator */}
{tip.status === 'pending' ? (
<div className="w-16 h-16 bg-[#fbeb0a] rounded-full flex items-center justify-center">
<span className="text-3xl">🎁</span>
</div>
) : tip.status === 'claimed' ? (
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
) : (
<div className="w-16 h-16 bg-zinc-700 rounded-full flex items-center justify-center">
<span className="text-3xl text-zinc-400">X</span>
</div>
)}
{/* Tip info */}
<div className="bg-zinc-900 rounded-xl p-6 w-full">
{coin && (
<div className="flex items-center justify-center gap-3 mb-4">
<Image src={coin.icon} alt={coin.name} width={48} height={48} />
<span className="text-2xl font-bold">{formatAmount(tip.amount, tip.asset)}</span>
</div>
)}
<div className="text-sm text-zinc-400 space-y-1">
<p>Status: <span className={
tip.status === 'pending' ? 'text-[#fbeb0a]' :
tip.status === 'claimed' ? 'text-green-400' :
'text-zinc-500'
}>{tip.status}</span></p>
<p>Created: {new Date(tip.created_at).toLocaleDateString()}</p>
</div>
</div>
{/* 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://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
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'}
</h2>
{hasExtension ? (
<div className="text-zinc-400 text-sm space-y-2">
<p>To claim this tip:</p>
<ol className="text-left list-decimal list-inside space-y-1">
<li>Click the Smirk extension icon</li>
<li>Go to <strong>Inbox</strong></li>
<li>Click <strong>Claim</strong> on this tip</li>
</ol>
</div>
) : (
<div className="text-zinc-400 text-sm space-y-2">
<p>You need the Smirk Wallet extension to claim tips.</p>
<a
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
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>
)}
{/* Public tip with missing fragment key */}
{tip.status === 'pending' && tip.is_public && !fragmentKey && (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-red-400">Incomplete Link</h2>
<div className="text-zinc-400 text-sm space-y-2">
<p>This is a public tip, but the claim link is incomplete.</p>
<p>Make sure you have the <strong>full URL</strong> including the secret key after the <code className="bg-zinc-800 px-1 rounded">#</code> symbol.</p>
<p className="text-zinc-500 text-xs mt-4">
Example: smirk.cash/tip/abc123<strong>#secretKey</strong>
</p>
</div>
</div>
)}
{tip.status === 'claimed' && !claimSuccess && (
<p className="text-green-400">This tip has already been claimed.</p>
)}
{tip.status !== 'pending' && tip.status !== 'claimed' && (
<p className="text-zinc-500">This tip is no longer available.</p>
)}
<Link
href="/"
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
>
Go Home
</Link>
</div>
)}
</div>
);
}
+45 -333
View File
@@ -1,339 +1,51 @@
'use client';
import type { Metadata } from 'next';
import { getPublicTipInfo } from '@/lib/api';
import TipPageClient, { formatAmount } from './TipPageClient';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Image from 'next/image';
import Link from 'next/link';
import { getPublicTipInfo, type PublicTipInfo } from '@/lib/api';
const COINS: Record<string, { name: string; icon: string; divisor: number; decimals: number }> = {
btc: { name: 'Bitcoin', icon: '/coins/bitcoin.svg', divisor: 100_000_000, decimals: 8 },
ltc: { name: 'Litecoin', icon: '/coins/litecoin.svg', divisor: 100_000_000, decimals: 8 },
xmr: { name: 'Monero', icon: '/coins/monero.svg', divisor: 1_000_000_000_000, decimals: 12 },
wow: { name: 'Wownero', icon: '/coins/wownero.svg', divisor: 100_000_000_000, decimals: 11 },
grin: { name: 'Grin', icon: '/coins/grin.svg', divisor: 1_000_000_000, decimals: 9 },
const COIN_NAMES: Record<string, string> = {
btc: 'Bitcoin',
ltc: 'Litecoin',
xmr: 'Monero',
wow: 'Wownero',
grin: 'Grin',
};
function formatAmount(amount: number, asset: string): string {
const coin = COINS[asset.toLowerCase()];
if (!coin) return `${amount} ${asset.toUpperCase()}`;
const value = amount / coin.divisor;
return `${value.toFixed(Math.min(coin.decimals, 8))} ${asset.toUpperCase()}`;
type Props = {
params: Promise<{ id: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
try {
const tip = await getPublicTipInfo(id);
const coinName = COIN_NAMES[tip.asset.toLowerCase()] || tip.asset.toUpperCase();
const amount = formatAmount(tip.amount, tip.asset);
const statusText = tip.status === 'pending' ? 'Claim it now!' : `Status: ${tip.status}`;
return {
title: `${amount} ${coinName} Tip`,
description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`,
openGraph: {
title: `${amount} ${coinName} Tip`,
description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`,
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: `${amount} ${coinName} Tip`,
description: `Someone sent you ${amount} via Smirk Wallet. ${statusText}`,
},
};
} catch {
return {
title: 'Crypto Tip',
description: 'Claim your crypto tip with Smirk Wallet.',
};
}
}
export default function TipPage() {
const params = useParams();
const tipId = params.id as string;
const [tip, setTip] = useState<PublicTipInfo | null>(null);
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
const check = () => setHasExtension(!!window.smirk);
check();
const timeout = setTimeout(check, 500);
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;
async function loadTip() {
try {
setLoading(true);
setError(null);
const tipInfo = await getPublicTipInfo(tipId);
setTip(tipInfo);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load tip');
} finally {
setLoading(false);
}
}
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 (
<div className="min-h-screen flex flex-col items-center justify-center p-8">
{/* Logo */}
<div className="mb-8 flex flex-col items-center">
<Link href="/" className="flex flex-col items-center">
<h1 className="creepster-text text-3xl mb-2">SMIRK WALLET</h1>
<div className="logo-glow">
<Image
src="/logo.svg"
alt="Smirk"
width={100}
height={100}
priority
/>
</div>
</Link>
</div>
{/* Loading state */}
{loading && (
<div className="flex flex-col items-center gap-4">
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
<p className="text-zinc-400">Loading tip...</p>
</div>
)}
{/* Error state */}
{error && !loading && (
<div className="flex flex-col items-center gap-6 text-center">
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center">
<span className="text-3xl">!</span>
</div>
<div>
<h2 className="text-xl font-semibold mb-2">Tip Not Found</h2>
<p className="text-zinc-400 text-sm max-w-md">
{error}
</p>
</div>
<Link
href="/"
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
>
Go Home
</Link>
</div>
)}
{/* Tip found */}
{tip && !loading && (
<div className="flex flex-col items-center gap-6 text-center max-w-md">
{/* Status indicator */}
{tip.status === 'pending' ? (
<div className="w-16 h-16 bg-[#fbeb0a] rounded-full flex items-center justify-center">
<span className="text-3xl">🎁</span>
</div>
) : tip.status === 'claimed' ? (
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center">
<svg
className="w-8 h-8 text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
) : (
<div className="w-16 h-16 bg-zinc-700 rounded-full flex items-center justify-center">
<span className="text-3xl text-zinc-400">X</span>
</div>
)}
{/* Tip info */}
<div className="bg-zinc-900 rounded-xl p-6 w-full">
{coin && (
<div className="flex items-center justify-center gap-3 mb-4">
<Image src={coin.icon} alt={coin.name} width={48} height={48} />
<span className="text-2xl font-bold">{formatAmount(tip.amount, tip.asset)}</span>
</div>
)}
<div className="text-sm text-zinc-400 space-y-1">
<p>Status: <span className={
tip.status === 'pending' ? 'text-[#fbeb0a]' :
tip.status === 'claimed' ? 'text-green-400' :
'text-zinc-500'
}>{tip.status}</span></p>
<p>Created: {new Date(tip.created_at).toLocaleDateString()}</p>
</div>
</div>
{/* 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'}
</h2>
{hasExtension ? (
<div className="text-zinc-400 text-sm space-y-2">
<p>To claim this tip:</p>
<ol className="text-left list-decimal list-inside space-y-1">
<li>Click the Smirk extension icon</li>
<li>Go to <strong>Inbox</strong></li>
<li>Click <strong>Claim</strong> on this tip</li>
</ol>
</div>
) : (
<div className="text-zinc-400 text-sm space-y-2">
<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>
)}
{/* Public tip with missing fragment key */}
{tip.status === 'pending' && tip.is_public && !fragmentKey && (
<div className="space-y-4">
<h2 className="text-xl font-semibold text-red-400">Incomplete Link</h2>
<div className="text-zinc-400 text-sm space-y-2">
<p>This is a public tip, but the claim link is incomplete.</p>
<p>Make sure you have the <strong>full URL</strong> including the secret key after the <code className="bg-zinc-800 px-1 rounded">#</code> symbol.</p>
<p className="text-zinc-500 text-xs mt-4">
Example: smirk.cash/tip/abc123<strong>#secretKey</strong>
</p>
</div>
</div>
)}
{tip.status === 'claimed' && !claimSuccess && (
<p className="text-green-400">This tip has already been claimed.</p>
)}
{tip.status !== 'pending' && tip.status !== 'claimed' && (
<p className="text-zinc-500">This tip is no longer available.</p>
)}
<Link
href="/"
className="px-6 py-2 border border-zinc-700 rounded-lg hover:border-zinc-500 transition-colors text-sm"
>
Go Home
</Link>
</div>
)}
</div>
);
export default async function TipPage({ params }: Props) {
const { id } = await params;
return <TipPageClient tipId={id} />;
}
+1 -1
View File
@@ -280,7 +280,7 @@ export default function TipsPage() {
<p className="text-yellow-400 text-sm">
Install the{' '}
<a
href="https://chromewebstore.google.com/detail/smirk/placeholder"
href="https://chromewebstore.google.com/detail/smirk-wallet/mbohhckapcchfmanheggbkekofhajnci"
target="_blank"
rel="noopener noreferrer"
className="underline"