diff --git a/.gitignore b/.gitignore
index b721bff..c7fb275 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index faeba2f..960a455 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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({
diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx
new file mode 100644
index 0000000..8a128af
--- /dev/null
+++ b/src/app/opengraph-image.tsx
@@ -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(
+ (
+
+ {/* Smirk emoji as placeholder for logo */}
+
😏
+
+ Smirk Wallet
+
+
+ BTC • LTC • XMR • WOW • GRIN
+
+
+ Multi-coin crypto wallet & tipping extension
+
+
+ ),
+ { ...size }
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index ecc17ca..1585dfc 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -202,7 +202,7 @@ export default function Home() {
GET SMIRKED
+ );
+}
diff --git a/src/app/tip/[id]/page.tsx b/src/app/tip/[id]/page.tsx
index 649ec1b..655fd9c 100644
--- a/src/app/tip/[id]/page.tsx
+++ b/src/app/tip/[id]/page.tsx
@@ -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 = {
- 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 = {
+ 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 {
+ 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(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [hasExtension, setHasExtension] = useState(null);
- const [fragmentKey, setFragmentKey] = useState(null);
- const [claiming, setClaiming] = useState(false);
- const [claimError, setClaimError] = useState(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 (
-
- {/* Logo */}
-
-
-
SMIRK WALLET
-
-
-
-
-
-
- {/* Loading state */}
- {loading && (
-
- )}
-
- {/* Error state */}
- {error && !loading && (
-
-
- !
-
-
-
Tip Not Found
-
- {error}
-
-
-
- Go Home
-
-
- )}
-
- {/* Tip found */}
- {tip && !loading && (
-
- {/* Status indicator */}
- {tip.status === 'pending' ? (
-
- 🎁
-
- ) : tip.status === 'claimed' ? (
-
- ) : (
-
- X
-
- )}
-
- {/* Tip info */}
-
- {coin && (
-
-
- {formatAmount(tip.amount, tip.asset)}
-
- )}
-
-
-
Status: {tip.status}
-
Created: {new Date(tip.created_at).toLocaleDateString()}
-
-
-
- {/* Claim success */}
- {claimSuccess && (
-
-
Claimed!
-
- Funds are being sent to your wallet.
-
-
- TX: {claimSuccess.txid}
-
-
- )}
-
- {/* Needs confirmations */}
- {needsConfirmations && !claimSuccess && (
-
-
Waiting for Confirmations
-
-
This tip is still confirming on the blockchain.
-
- {tip?.funding_confirmations ?? 0}
- {' / '}
- {tip?.confirmations_required ?? 0}
- {' confirmations'}
-
-
- Refresh this page to check progress.
-
-
-
- )}
-
- {/* Claim button for public tips */}
- {isClaimable && !claimSuccess && (
-
-
- {hasExtension ? 'Ready to Claim!' : 'Install Smirk Wallet'}
-
-
- {hasExtension ? (
-
-
- {claimError && (
-
{claimError}
- )}
-
- ) : (
-
- )}
-
- )}
-
- {/* Targeted tip instructions (no fragment key) */}
- {tip.status === 'pending' && !fragmentKey && !tip.is_public && (
-
-
- {hasExtension ? 'Claim This Tip' : 'Install Smirk Wallet'}
-
-
- {hasExtension ? (
-
-
To claim this tip:
-
- - Click the Smirk extension icon
- - Go to Inbox
- - Click Claim on this tip
-
-
- ) : (
-
- )}
-
- )}
-
- {/* Public tip with missing fragment key */}
- {tip.status === 'pending' && tip.is_public && !fragmentKey && (
-
-
Incomplete Link
-
-
This is a public tip, but the claim link is incomplete.
-
Make sure you have the full URL including the secret key after the # symbol.
-
- Example: smirk.cash/tip/abc123#secretKey
-
-
-
- )}
-
- {tip.status === 'claimed' && !claimSuccess && (
-
This tip has already been claimed.
- )}
-
- {tip.status !== 'pending' && tip.status !== 'claimed' && (
-
This tip is no longer available.
- )}
-
-
- Go Home
-
-
- )}
-
- );
+export default async function TipPage({ params }: Props) {
+ const { id } = await params;
+ return ;
}
diff --git a/src/app/tips/page.tsx b/src/app/tips/page.tsx
index fcbc999..b11a5a1 100644
--- a/src/app/tips/page.tsx
+++ b/src/app/tips/page.tsx
@@ -280,7 +280,7 @@ export default function TipsPage() {
Install the{' '}