Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 48 additions & 18 deletions src/app/cards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<StoredCard[]>([]);
const [showAddCard, setShowAddCard] = useState(false);
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -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) => {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)...");
Expand All @@ -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 = {
Expand Down Expand Up @@ -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:", {
Expand All @@ -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...");

Expand Down Expand Up @@ -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("");
Expand Down Expand Up @@ -325,7 +355,7 @@ export default function CardsPage() {
</div>
<button
onClick={() => setShowAddCard(true)}
disabled={!isConnected}
disabled={!isConnected && !isSimulationMode}
className="px-6 py-3 bg-gradient-to-r from-primary to-accent rounded-full text-white font-medium hover:shadow-glow transition-all flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="w-5 h-5" />
Expand All @@ -334,7 +364,7 @@ export default function CardsPage() {
</div>

{/* Wallet Connection Warning */}
{!isConnected && (
{!isConnected && !isSimulationMode && (
<div className="glass-card border-yellow-500/30 bg-yellow-500/10">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-yellow-500/20 flex items-center justify-center flex-shrink-0">
Expand All @@ -351,7 +381,7 @@ export default function CardsPage() {
)}

{/* On-chain config warning */}
{isConnected && !isOnChainConfigured() && (
{isConnected && !isOnChainConfigured() && !isSimulationMode && (
<div className="glass-card border-yellow-500/30 bg-yellow-500/10">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-yellow-500/20 flex items-center justify-center flex-shrink-0">
Expand Down Expand Up @@ -468,12 +498,12 @@ export default function CardsPage() {
{/* Add New Card Placeholder */}
<button
onClick={() => setShowAddCard(true)}
disabled={!isConnected}
disabled={!isConnected && !isSimulationMode}
className="premium-card min-h-[200px] flex flex-col items-center justify-center text-muted-foreground hover:text-foreground border-dashed disabled:opacity-50 disabled:cursor-not-allowed"
>
<Plus className="w-12 h-12 mb-4" />
<div className="font-semibold">Add New Card</div>
{!isConnected && <div className="text-xs mt-2">Connect wallet first</div>}
{!isConnected && !isSimulationMode && <div className="text-xs mt-2">Connect wallet first</div>}
</button>
</div>

Expand Down
36 changes: 30 additions & 6 deletions src/components/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 (<>
<nav className="sticky top-0 z-40 w-full border-b border-border/40 glass backdrop-blur-lg">
<div className="container mx-auto px-4">
<div className="flex h-16 items-center justify-between">
Expand Down Expand Up @@ -69,9 +69,33 @@ export function Navigation() {
})}
</div>

{/* Right side: wallet + mobile menu button */}
{/* Right side: Simulation Mode + wallet + mobile menu button */}
<div className="flex items-center gap-2">
{isConnected ? (
{/* Simulation Mode button */}
<button
onClick={toggleSimulationMode}
className={cn(
"px-4 py-2 rounded-full text-sm font-semibold border transition-all flex items-center gap-1.5",
isSimulationMode
? "bg-green-500/10 border-green-500/50 text-green-400 hover:bg-green-500/20"
: "border-primary/60 text-primary hover:bg-primary/10 hover:border-primary"
)}
>
<Zap className="w-3.5 h-3.5" />
<span className="hidden sm:inline">
{isSimulationMode ? "Exit Simulation" : "Simulation Mode"}
</span>
</button>

{isSimulationMode ? (
<div className="hidden sm:flex items-center gap-2 px-4 py-2 bg-green-500/10 rounded-full border border-green-500/20">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span className="text-sm font-medium text-green-400">Simulation Active</span>
</div>
) : isConnected ? (
<div className="flex items-center gap-2">
<span className="hidden sm:inline px-3 py-1.5 bg-muted rounded-full text-sm font-mono">
{truncateAddress(publicKey!)}
Expand Down Expand Up @@ -133,5 +157,5 @@ export function Navigation() {
)}
</div>
</nav>
);
</>);
}
21 changes: 21 additions & 0 deletions src/context/WalletContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface WalletContextType {
signAuthEntry: (message: string) => Promise<SignResult>;
deriveEncryptionKey: () => Promise<Uint8Array>;
kit: StellarWalletsKit | null;
isSimulationMode: boolean;
toggleSimulationMode: () => void;
}

const WalletContext = createContext<WalletContextType>({
Expand All @@ -33,12 +35,15 @@ const WalletContext = createContext<WalletContextType>({
signAuthEntry: async () => ({ signature: "", signerAddress: "" }),
deriveEncryptionKey: async () => new Uint8Array(),
kit: null,
isSimulationMode: false,
toggleSimulationMode: () => { },
});

export function WalletProvider({ children }: { children: ReactNode }) {
const [publicKey, setPublicKey] = useState<string | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [kit, setKit] = useState<StellarWalletsKit | null>(null);
const [isSimulationMode, setIsSimulationMode] = useState(false);

// Initialize the wallet kit on client side
useEffect(() => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -178,6 +197,8 @@ export function WalletProvider({ children }: { children: ReactNode }) {
signAuthEntry,
deriveEncryptionKey,
kit,
isSimulationMode,
toggleSimulationMode,
}}
>
{children}
Expand Down
Loading