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
5 changes: 5 additions & 0 deletions src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export default function SettingsPage() {
Dashboard Widgets
</a>
</li>
<li>
<a href="/settings/wallet-verification" className="text-indigo-400 hover:text-indigo-300">
Wallet Verification
</a>
</li>
</ul>
</section>
</main>
Expand Down
76 changes: 76 additions & 0 deletions src/app/settings/security/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { apiFetch } from '@/lib/apiClient';
import {
clearWalletConnection,
getWalletConnection,
getWalletPersistencePreference,
setWalletPersistencePreference,
type WalletConnection,
} from '@/lib/walletConnection';

interface SecuritySettingsResponse {
mfaEnabled: boolean;
Expand All @@ -24,10 +31,14 @@ export default function SecuritySettingsPage() {
const [message, setMessage] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [userId, setUserId] = useState('default-user');
const [wallet, setWallet] = useState<WalletConnection | null>(null);
const [persistWallet, setPersistWallet] = useState(true);

useEffect(() => {
const stored = localStorage.getItem('stellarsplit_wallet_address') || 'default-user';
setUserId(stored);
setWallet(getWalletConnection());
setPersistWallet(getWalletPersistencePreference());

const load = async () => {
try {
Expand All @@ -43,6 +54,24 @@ export default function SecuritySettingsPage() {
load();
}, []);

const handleDisconnectWallet = () => {
clearWalletConnection();
setWallet(null);
setMessage('Wallet disconnected.');
};

const handleTogglePersistence = (next: boolean) => {
setPersistWallet(next);
setWalletPersistencePreference(next);
if (!next) {
clearWalletConnection();
setWallet(null);
setMessage('Wallet connection persistence disabled and stored connection cleared.');
} else {
setMessage('Wallet connection will be remembered on this device.');
}
};

const refreshSettings = async (id: string) => {
try {
const response = await fetch(`/api/settings/mfa?userId=${encodeURIComponent(id)}`);
Expand Down Expand Up @@ -157,6 +186,53 @@ export default function SecuritySettingsPage() {
<Link href="/settings" className="text-sm text-indigo-400 hover:text-indigo-300">← Back to settings</Link>
</div>

<section className="rounded-2xl border border-gray-800 bg-gray-900/80 p-6 shadow-xl">
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-xl font-semibold">Wallet connection</h2>
<p className="mt-1 text-sm text-gray-400">Your wallet connection is remembered on this device so you can pick up where you left off.</p>
</div>
<div className="rounded-full border border-indigo-500/30 bg-indigo-500/10 px-3 py-1 text-sm text-indigo-300">
{wallet ? 'Connected' : 'Disconnected'}
</div>
</div>

<div className="mt-6 grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
<div className="rounded-xl border border-gray-800 bg-gray-950/70 p-4">
<p className="text-sm font-medium text-gray-200">Current connection</p>
{wallet ? (
<>
<p className="mt-2 text-sm text-gray-400">Wallet: {wallet.walletType}</p>
<p className="mt-1 break-all text-sm text-gray-400">Address: {wallet.address}</p>
</>
) : (
<p className="mt-2 text-sm text-gray-400">No wallet is currently connected.</p>
)}
<button
type="button"
onClick={handleDisconnectWallet}
disabled={!wallet}
className="mt-4 rounded-lg border border-gray-700 px-4 py-2 text-sm font-semibold text-gray-300 hover:bg-gray-800 disabled:opacity-50"
>
Disconnect wallet
</button>
</div>

<div className="rounded-xl border border-gray-800 bg-gray-950/70 p-4">
<p className="text-sm font-medium text-gray-200">Privacy</p>
<label className="mt-3 flex items-start gap-3 text-sm text-gray-400">
<input
type="checkbox"
checked={persistWallet}
onChange={(event) => handleTogglePersistence(event.target.checked)}
className="mt-1 h-4 w-4 rounded border-gray-700 bg-gray-800"
/>
<span>Remember my wallet connection on this device. Turning this off clears any stored connection data.</span>
</label>
</div>
</div>
</section>

<section className="rounded-2xl border border-gray-800 bg-gray-900/80 p-6 shadow-xl">
<div className="flex items-center justify-between gap-4">
<div>
Expand Down
113 changes: 112 additions & 1 deletion src/app/settings/verify/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,22 @@ import {
isValidUrl,
type VerificationRequest,
} from '@/lib/verificationRequests';
import { useAddressValidation } from '@/hooks/useAddressValidation';
import { WalletAddress } from '@/components/WalletAddress';

export default function VerifyIdentityPage() {
const [mounted, setMounted] = useState(false);
const [address, setAddress] = useState<string | null>(null);
const [walletInput, setWalletInput] = useState('');
const {
input: validatedInput,
setInput: setValidatedInput,
result: validationResult,
loading: validating,
error: validationError,
validate: validateAddress,
reset: resetValidation,
} = useAddressValidation();
const [attestation, setAttestation] = useState<CreatorAttestation | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -188,7 +200,7 @@ export default function VerifyIdentityPage() {
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm text-gray-400">Connected Address:</span>
<span className="font-mono text-sm text-gray-200 break-all">{address}</span>
<WalletAddress address={address} />
</div>
</div>
) : (
Expand All @@ -198,6 +210,105 @@ export default function VerifyIdentityPage() {
)}
</div>

{/* Wallet Address Verification */}
<div className="bg-gray-900 rounded-lg p-6 mb-6">
<h2 className="text-lg font-semibold mb-2">Verify a Wallet Address</h2>
<p className="text-sm text-gray-400 mb-4">
Enter a Stellar address or a federation address (e.g. <span className="font-mono">name*domain.com</span>)
to validate it and resolve it to its Stellar account.
</p>

<div className="flex flex-col sm:flex-row gap-2">
<input
type="text"
value={validatedInput}
onChange={(e) => setValidatedInput(e.target.value)}
placeholder="G... or name*domain.com"
aria-label="Wallet address or federation address"
className="flex-1 px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-sm font-mono"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void validateAddress();
}
}}
/>
<button
type="button"
onClick={() => void validateAddress()}
disabled={validating || !validatedInput.trim()}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded-lg transition-colors text-sm font-medium"
>
{validating ? 'Validating...' : 'Validate'}
</button>
{(validationResult || validationError) && (
<button
type="button"
onClick={resetValidation}
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-gray-200 rounded-lg transition-colors text-sm"
>
Clear
</button>
)}
</div>

{validationError && (
<div className="mt-4 p-4 bg-red-900/20 border border-red-900 rounded-lg text-red-300 text-sm">
{validationError}
</div>
)}

{validationResult && (
<div className="mt-4 space-y-3">
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 text-sm px-2 py-1 rounded-full bg-green-900 text-green-300">
✓ Valid address
</span>
{validationResult.federation && (
<span className="inline-flex items-center gap-1 text-sm px-2 py-1 rounded-full bg-indigo-900 text-indigo-300">
Resolved from federation
</span>
)}
</div>

{validationResult.federation && (
<div className="space-y-1 text-sm">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
<span className="text-gray-400">Federation address:</span>
<span className="font-mono text-gray-200 break-all">
{validationResult.federation}
</span>
</div>
</div>
)}

<div className="space-y-1 text-sm">
<span className="text-gray-400">Stellar address:</span>
<WalletAddress address={validationResult.address} />
</div>

{validationResult.memo && (
<div className="space-y-1 text-sm">
<span className="text-gray-400">Memo:</span>
<span className="font-mono text-gray-200 break-all">
{validationResult.memo.type}: {validationResult.memo.value}
</span>
</div>
)}

{address && validationResult.address !== address && (
<button
type="button"
onClick={() => setWalletInput(validationResult.address)}
className="text-sm text-indigo-400 hover:text-indigo-300 transition-colors"
>
Use this address
</button>
)}
</div>
)}
</div>

{/* Verification Status */}
<div className="bg-gray-900 rounded-lg p-6 mb-6">
<h2 className="text-lg font-semibold mb-4">Verification Status</h2>
Expand Down
74 changes: 73 additions & 1 deletion src/components/NetworkStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use client";

import { useEffect, useState, useRef, useCallback } from "react";
import { useWalletBalance } from "../hooks/useWalletBalance";
import { useWalletConnect } from "../hooks/useWalletConnect";

const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL ?? "";
const POLL_MS = 60_000;
Expand Down Expand Up @@ -67,6 +69,9 @@ export default function NetworkStatus() {
const [browserOffline, setBrowserOffline] = useState(false);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);

const { lowBalance, lowBalanceAssets } = useWalletBalance();
const { status: wcStatus } = useWalletConnect();

const poll = useCallback(async () => {
const s = await checkHealth();
setStatus((prev) => {
Expand Down Expand Up @@ -153,6 +158,36 @@ export default function NetworkStatus() {
}`
: "Soroban RPC: checking…";

// ── WalletConnect connection indicator styles ───────────────────────────────

const wcConnected = wcStatus === "connected";
const wcPending = wcStatus === "connecting" || wcStatus === "pairing";

const wcDotColor = wcConnected
? "bg-blue-400"
: wcPending
? "bg-blue-400 animate-pulse"
: wcStatus === "error" || wcStatus === "disconnected"
? "bg-gray-500"
: "bg-gray-400";

const wcLabel = wcConnected
? "Wallet connected"
: wcPending
? "Connecting…"
: wcStatus === "error"
? "Wallet error"
: "Wallet disconnected";

const wcTooltip = `WalletConnect: ${wcLabel}`;

// ── Low balance warning styles ──────────────────────────────────────────────

const lowBalanceTooltip =
lowBalanceAssets && lowBalanceAssets.length > 0
? `Low balance: ${lowBalanceAssets.join(", ")}`
: "One or more token balances are low";

// ── Network badge styles ─────────────────────────────────────────────────────

const networkBadgeClasses =
Expand Down Expand Up @@ -209,8 +244,29 @@ export default function NetworkStatus() {
</div>
)}

{/* ── Network badge + RPC health ── */}
{/* ── Network badge + RPC health + WalletConnect + low balance ── */}
<div className="flex items-center gap-2">
{/* Low balance warning badge */}
{lowBalance && (
<div className="group relative">
<span
className="inline-flex items-center gap-1 rounded-full border border-orange-500/40 bg-orange-500/15 px-2 py-0.5 text-xs font-semibold tracking-wide text-orange-300 select-none"
role="status"
aria-label="Low balance warning"
>
<span aria-hidden="true">⚠️</span>
<span className="hidden sm:inline">Low balance</span>
</span>
{/* Tooltip */}
<span
role="tooltip"
className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 hidden group-hover:block whitespace-nowrap rounded bg-gray-900 border border-gray-700 px-2 py-1 text-xs text-gray-200 shadow-lg"
>
{lowBalanceTooltip}
</span>
</div>
)}

{/* Permanent Mainnet / Testnet badge */}
<div className="group relative">
<span
Expand Down Expand Up @@ -244,6 +300,22 @@ export default function NetworkStatus() {
{rpcTooltip}
</span>
</div>

{/* WalletConnect connection dot */}
<div className="group relative flex items-center gap-1.5 cursor-default select-none">
<span
className={`inline-block w-2 h-2 rounded-full shrink-0 ${wcDotColor}`}
aria-hidden="true"
/>
<span className="text-xs text-gray-400 hidden sm:inline">{wcLabel}</span>
{/* Tooltip */}
<span
role="tooltip"
className="pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 hidden group-hover:block whitespace-nowrap rounded bg-gray-900 border border-gray-700 px-2 py-1 text-xs text-gray-200 shadow-lg"
>
{wcTooltip}
</span>
</div>
</div>
</>
);
Expand Down
Loading
Loading