diff --git a/src/app/cards/page.tsx b/src/app/cards/page.tsx index 2c2318e..f1ad3f8 100644 --- a/src/app/cards/page.tsx +++ b/src/app/cards/page.tsx @@ -29,7 +29,7 @@ interface CardFormData { } export default function CardsPage() { - const { publicKey, isConnected, deriveEncryptionKey, kit } = useWallet(); + const { publicKey, isConnected, deriveEncryptionKey, kit, isSimulationMode } = useWallet(); const [cards, setCards] = useState([]); const [showAddCard, setShowAddCard] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -45,13 +45,18 @@ export default function CardsPage() { // Load saved cards from localStorage on mount (cache / fallback when on-chain is not configured) useEffect(() => { - if (publicKey) { - const savedCards = localStorage.getItem(`tychee_cards_${publicKey}`); + const key = isSimulationMode ? "sim_user" : publicKey; + if (key) { + const savedCards = localStorage.getItem(`tychee_cards_${key}`); if (savedCards) { setCards(JSON.parse(savedCards)); + } else { + setCards([]); } + } else { + setCards([]); } - }, [publicKey]); + }, [publicKey, isSimulationMode]); // Close modal on Escape key const handleEscKey = useCallback((e: KeyboardEvent) => { @@ -104,7 +109,7 @@ export default function CardsPage() { e.preventDefault(); setError(null); - if (!isConnected || !publicKey) { + if (!isConnected && !publicKey && !isSimulationMode) { setError("Please connect your wallet first"); return; } @@ -148,9 +153,17 @@ export default function CardsPage() { network: network, }; - // Step 4: Derive user-owned encryption key from wallet signature - setLoadingStep("Deriving encryption key from wallet..."); - const encryptionKey = await deriveEncryptionKey(); + // Step 4: Derive user-owned encryption key + let encryptionKey: Uint8Array; + if (isSimulationMode) { + setLoadingStep("Simulation: Generating demo encryption key..."); + const seed = new TextEncoder().encode("tychee:demo:v1:no-wallet-required"); + const buf = await crypto.subtle.digest("SHA-256", seed as unknown as BufferSource); + encryptionKey = new Uint8Array(buf); + } else { + setLoadingStep("Deriving encryption key from wallet..."); + encryptionKey = await deriveEncryptionKey(); + } // Step 5: Encrypt card using browser-compatible CardTokenizer setLoadingStep("Encrypting card data (AES-256-GCM)..."); @@ -171,7 +184,18 @@ export default function CardsPage() { // Step 6: Store on-chain with wallet signature let txHash: string | undefined; - if (kit && isOnChainConfigured()) { + if (isSimulationMode) { + setLoadingStep("Simulation: Saving token hash..."); + await new Promise((r) => setTimeout(r, 1400)); + const raw = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(`demo:${tokenHash}:${Date.now()}`) as unknown as BufferSource + ); + const hex = Array.from(new Uint8Array(raw)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + txHash = `0x${hex}`; + } else if (kit && isOnChainConfigured() && publicKey) { setLoadingStep("Requesting wallet signature for on-chain storage..."); const storeParams: StoreTokenParams = { @@ -220,9 +244,10 @@ export default function CardsPage() { }; // Save to state and localStorage (serves as local cache for on-chain data) + const storageKey = isSimulationMode ? "sim_user" : publicKey; const updatedCards = [...cards, newCard]; setCards(updatedCards); - localStorage.setItem(`tychee_cards_${publicKey}`, JSON.stringify(updatedCards)); + localStorage.setItem(`tychee_cards_${storageKey}`, JSON.stringify(updatedCards)); // Log success console.log("Card tokenized:", { @@ -249,13 +274,17 @@ export default function CardsPage() { }; const handleRevokeCard = async (cardId: string) => { - if (!publicKey) return; + if (!publicKey && !isSimulationMode) return; if (!confirm("Are you sure you want to revoke this card? This action cannot be undone.")) return; const card = cards.find(c => c.id === cardId); // If the card was stored on-chain, revoke it on-chain first - if (card?.txHash && card?.tokenHash && kit && isOnChainConfigured()) { + if (isSimulationMode) { + setIsLoading(true); + setLoadingStep("Simulation: Revoking card..."); + await new Promise((r) => setTimeout(r, 1000)); + } else if (card?.txHash && card?.tokenHash && kit && isOnChainConfigured() && publicKey) { setIsLoading(true); setLoadingStep("Revoking card on-chain..."); @@ -284,9 +313,10 @@ export default function CardsPage() { } // Remove from local state and cache + const storageKey = isSimulationMode ? "sim_user" : publicKey; const updatedCards = cards.filter(c => c.id !== cardId); setCards(updatedCards); - localStorage.setItem(`tychee_cards_${publicKey}`, JSON.stringify(updatedCards)); + localStorage.setItem(`tychee_cards_${storageKey}`, JSON.stringify(updatedCards)); setIsLoading(false); setLoadingStep(""); @@ -325,7 +355,7 @@ export default function CardsPage() { diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 92b464a..5cec32c 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; -import { CreditCard, TrendingUp, Gift, Store, Ticket, Users, Wallet, LogOut, Menu, X } from "lucide-react"; +import { CreditCard, TrendingUp, Gift, Store, Ticket, Users, Wallet, LogOut, Menu, X, Zap } from "lucide-react"; import { cn } from "@/lib/utils"; import { useWallet } from "@/context/WalletContext"; @@ -18,14 +18,14 @@ const navigation = [ export function Navigation() { const pathname = usePathname(); - const { publicKey, isConnected, isConnecting, connect, disconnect } = useWallet(); + const { publicKey, isConnected, isConnecting, connect, disconnect, isSimulationMode, toggleSimulationMode } = useWallet(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const truncateAddress = (address: string) => { return `${address.slice(0, 4)}...${address.slice(-4)}`; }; - return ( + return (<> - ); + ); } diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 28133ae..073690b 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -22,6 +22,8 @@ interface WalletContextType { signAuthEntry: (message: string) => Promise; deriveEncryptionKey: () => Promise; kit: StellarWalletsKit | null; + isSimulationMode: boolean; + toggleSimulationMode: () => void; } const WalletContext = createContext({ @@ -33,12 +35,15 @@ const WalletContext = createContext({ signAuthEntry: async () => ({ signature: "", signerAddress: "" }), deriveEncryptionKey: async () => new Uint8Array(), kit: null, + isSimulationMode: false, + toggleSimulationMode: () => { }, }); export function WalletProvider({ children }: { children: ReactNode }) { const [publicKey, setPublicKey] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [kit, setKit] = useState(null); + const [isSimulationMode, setIsSimulationMode] = useState(false); // Initialize the wallet kit on client side useEffect(() => { @@ -54,6 +59,20 @@ export function WalletProvider({ children }: { children: ReactNode }) { if (savedPublicKey) { setPublicKey(savedPublicKey); } + + // Check simulation mode + const savedSimMode = localStorage.getItem("tychee_simulation_mode"); + if (savedSimMode === "true") { + setIsSimulationMode(true); + } + }, []); + + const toggleSimulationMode = useCallback(() => { + setIsSimulationMode(prev => { + const newVal = !prev; + localStorage.setItem("tychee_simulation_mode", String(newVal)); + return newVal; + }); }, []); const connect = useCallback(async () => { @@ -178,6 +197,8 @@ export function WalletProvider({ children }: { children: ReactNode }) { signAuthEntry, deriveEncryptionKey, kit, + isSimulationMode, + toggleSimulationMode, }} > {children}