Skip to content
Merged
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
30 changes: 30 additions & 0 deletions frontend/app/connect-wallet/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import React from "react";
import Link from "next/link";
import WalletConnectButton from "@/components/WalletConnectButton";

export default function ConnectWalletPage() {
return (
<div className="min-h-screen flex flex-col items-center justify-center p-6">
<div className="max-w-md w-full text-center space-y-6">
<h1 className="text-3xl font-bold tracking-tight">Connect Wallet</h1>
<p className="text-gray-600">
Connect your Freighter wallet to submit puzzle answers and claim
on-chain NFT rewards.
</p>

<div className="flex justify-center">
<WalletConnectButton />
</div>

<Link
href="/puzzles"
className="inline-block text-sm text-blue-600 hover:underline"
>
Back to puzzles
</Link>
</div>
</div>
);
}
101 changes: 101 additions & 0 deletions frontend/components/WalletConnectButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"use client";

import { useEffect } from "react";
import { Button } from "@/components/ui/button";
import LoadingSpinner from "@/components/ui/LoadingSpinner";
import { useWalletConnection, WALLET_STATUS } from "@/hooks/useWalletConnection";

/**
* localStorage key used to persist the last connected Stellar address so the
* button can show a stable session across page reloads (#691).
*/
const STORAGE_KEY = "stellarhunts.wallet.address";

/**
* Shorten a Stellar public key for display, e.g. GABCDE...WXYZ.
*/
function shortenAddress(address) {
if (!address) return "";
return `${address.slice(0, 4)}...${address.slice(-4)}`;
}

/**
* WalletConnectButton
* --------------------
* Freighter wallet connect button (#691). Wraps `useWalletConnection` to
* expose a single control that:
* - connects / disconnects via Freighter
* - displays the active account address
* - surfaces a user rejection or a missing extension
* - restores the last connected address across reloads
*/
export default function WalletConnectButton({ className }) {
const {
status,
address,
error,
isExtensionInstalled,
connect,
disconnect,
} = useWalletConnection();

const isConnecting = status === WALLET_STATUS.CONNECTING;
const isConnected = status === WALLET_STATUS.CONNECTED && Boolean(address);

// Persist the connected address so a reload keeps showing the active
// account instead of flashing back to the disconnected state.
useEffect(() => {
if (typeof window === "undefined") return;
if (isConnected && address) {
window.localStorage.setItem(STORAGE_KEY, address);
} else if (status === WALLET_STATUS.DISCONNECTED) {
window.localStorage.removeItem(STORAGE_KEY);
}
}, [isConnected, address, status]);

return (
<div className={className}>
{isConnected ? (
<div className="flex items-center gap-3">
<span
className="font-mono text-sm text-neutral-700 dark:text-neutral-200"
title={address}
>
{shortenAddress(address)}
</span>
<Button variant="outline" size="sm" onClick={disconnect}>
Disconnect
</Button>
</div>
) : (
<Button
size="sm"
onClick={connect}
disabled={isConnecting}
aria-busy={isConnecting}
>
{isConnecting ? (
<>
<LoadingSpinner size="sm" />
Connecting
</>
) : (
"Connect Wallet"
)}
</Button>
)}

{error && (
<p className="mt-2 text-xs text-red-500" role="alert">
{error}
</p>
)}

{!error && !isConnected && status === WALLET_STATUS.DISCONNECTED && !isExtensionInstalled && (
<p className="mt-2 text-xs text-neutral-500">
Freighter extension not detected.
</p>
)}
</div>
);
}