diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 24f7bfe3..c6ba24fa 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -75,34 +75,34 @@ jobs:
run: npm run build --prefix apps/web
playwright-e2e:
- name: E2E Tests (Playwright)
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: 'npm'
- cache-dependency-path: package-lock.json
- - name: Install dependencies
- run: npm install
- env:
- npm_config_fetch_timeout: 120000
- npm_config_fetch_retry_mintimeout: 20000
- npm_config_fetch_retry_maxtimeout: 120000
- npm_config_fetch_retries: 5
- - name: Install web dependencies
- run: npm install --prefix apps/web
- env:
- npm_config_fetch_timeout: 120000
- npm_config_fetch_retry_mintimeout: 20000
- npm_config_fetch_retry_maxtimeout: 120000
- npm_config_fetch_retries: 5
- - name: Install Playwright Browsers
- run: npx playwright install --with-deps chromium
- - name: Run E2E tests
- run: npm run test:e2e
- env:
- NEXT_PUBLIC_E2E: "true"
- NEXT_PUBLIC_API_URL: "http://localhost:3001"
\ No newline at end of file
+ name: E2E Tests (Playwright)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+ cache-dependency-path: package-lock.json
+ - name: Install dependencies
+ run: npm install
+ env:
+ npm_config_fetch_timeout: 120000
+ npm_config_fetch_retry_mintimeout: 20000
+ npm_config_fetch_retry_maxtimeout: 120000
+ npm_config_fetch_retries: 5
+ - name: Install web dependencies
+ run: npm install --prefix apps/web
+ env:
+ npm_config_fetch_timeout: 120000
+ npm_config_fetch_retry_mintimeout: 20000
+ npm_config_fetch_retry_maxtimeout: 120000
+ npm_config_fetch_retries: 5
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps chromium
+ - name: Run E2E tests
+ run: npm run test:e2e
+ env:
+ NEXT_PUBLIC_E2E: "true"
+ NEXT_PUBLIC_API_URL: "http://localhost:3001"
diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css
index 6a2126e3..187ad48b 100644
--- a/apps/web/app/globals.css
+++ b/apps/web/app/globals.css
@@ -1,53 +1,84 @@
@import "tailwindcss";
-@theme inline {
- --color-background: #09090b; /* zinc-950 */
- --color-foreground: #fafafa; /* zinc-50 */
-
- --color-primary: #6366f1; /* indigo-500 */
- --color-primary-foreground: #ffffff;
-
- --color-muted: #18181b; /* zinc-900 */
- --color-muted-foreground: #a1a1aa; /* zinc-400 */
-
- --color-border: #27272a; /* zinc-800 */
- --color-input: #27272a;
-
- --radius-xl: 12px;
- --radius-lg: 10px;
- --radius-md: 8px;
- --radius-sm: 4px;
-
- --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif;
- --font-mono: 'Geist Mono', ui-monospace, monospace;
+:root {
+ --background: #f4f4f5;
+ --foreground: #18181b;
+ --card: #ffffff;
+ --card-foreground: #18181b;
+ --muted: #e4e4e7;
+ --muted-foreground: #3f3f46;
+ --border: #d4d4d8;
+ --input: #d4d4d8;
+ --primary: #27272a;
+ --primary-foreground: #fafafa;
+ --accent: #e4e4e7;
+ --accent-foreground: #18181b;
+ --ring: #27272a;
+ --success: #22c55e;
+ --warning: #f59e0b;
+ --font-geist-sans:
+ Inter, "Segoe UI", "Helvetica Neue", Arial, ui-sans-serif, system-ui,
+ sans-serif;
+ --font-geist-mono:
+ "SFMono-Regular", "Cascadia Code", "Fira Code", Consolas, ui-monospace,
+ monospace;
}
-:root {
- --background: #09090b;
+.dark {
+ --background: #18181b;
--foreground: #fafafa;
- --primary: #6366f1;
- --border: #27272a;
- --muted: #18181b;
+ --card: #27272a;
+ --card-foreground: #fafafa;
+ --muted: #3f3f46;
--muted-foreground: #a1a1aa;
+ --border: #3f3f46;
+ --input: #3f3f46;
+ --primary: #6366f1;
+ --primary-foreground: #fafafa;
+ --accent: #27272a;
+ --accent-foreground: #fafafa;
+ --ring: #6366f1;
+ --success: #22c55e;
+ --warning: #f59e0b;
+ --zinc-900: #18181b;
+ --zinc-800: #27272a;
+ --zinc-700: #3f3f46;
+ --zinc-600: #52525b;
+ --indigo-500: #6366f1;
+ --indigo-400: #818cf8;
+ --indigo-300: #a5b4fc;
}
-body {
- background-color: var(--background);
- color: var(--foreground);
- font-family: var(--font-sans);
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-ring: var(--ring);
+ --color-success: var(--success);
+ --color-warning: var(--warning);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
-/* Glass effect for shells and cards */
-.glass-surface {
- background: rgba(24, 24, 27, 0.6);
- backdrop-filter: blur(12px);
- border: 1px solid rgba(39, 39, 42, 0.5);
+ --radius-xl: 12px;
+ --radius-lg: 10px;
+ --radius-md: 8px;
+ --radius-sm: 4px;
}
-.transition-smooth {
- transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+body {
+ background: var(--background);
+ color: var(--foreground);
+ font-family: var(--font-geist-sans), sans-serif;
+ @apply antialiased selection:bg-indigo-500/30;
}
button,
@@ -56,7 +87,8 @@ input,
textarea,
select {
transition-duration: 200ms;
- transition-property: color, background-color, border-color, box-shadow, opacity, transform;
+ transition-property: color, background-color, border-color, box-shadow,
+ opacity, transform;
transition-timing-function: ease;
}
@@ -65,37 +97,43 @@ select {
outline-offset: 2px;
}
-/* Responsive design utilities */
+.glass-surface {
+ background: color-mix(in srgb, var(--card) 82%, transparent);
+ backdrop-filter: blur(14px);
+}
+
+.transition-smooth {
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+}
+
@media (max-width: 640px) {
.responsive-text-xs {
font-size: 0.625rem;
line-height: 0.75rem;
}
-
+
.responsive-text-sm {
font-size: 0.75rem;
line-height: 1rem;
}
-
+
.responsive-gap {
gap: 0.5rem;
}
}
-/* High contrast mode support */
@media (prefers-contrast: high) {
:root {
--border: #000000;
--ring: #ffffff;
}
-
+
.dark {
--border: #ffffff;
--ring: #000000;
}
}
-/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
button,
a,
@@ -104,18 +142,13 @@ select {
select {
transition: none;
}
-
+
.animate-spin,
.animate-pulse {
animation: none;
}
}
-.glass-surface {
- background: color-mix(in srgb, var(--card) 82%, transparent);
- backdrop-filter: blur(14px);
-}
-
@keyframes shimmer {
0% {
background-position: 200% 0;
diff --git a/apps/web/components/auth/wallet-connect.tsx b/apps/web/components/auth/wallet-connect.tsx
new file mode 100644
index 00000000..623d72a1
--- /dev/null
+++ b/apps/web/components/auth/wallet-connect.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { useSyncExternalStore } from "react";
+import { Button } from "@/components/ui/button";
+import { useWalletStore } from "@/lib/store/use-wallet-store";
+import { connectWallet, getWalletsKit, signAuthMessage } from "@/lib/stellar";
+import { Loader2, Wallet, LogOut, AlertCircle, ChevronRight, User } from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+
+export function WalletConnect() {
+ const {
+ publicKey,
+ isConnected,
+ isConnecting,
+ setConnecting,
+ setConnection,
+ disconnect,
+ setError,
+ error
+ } = useWalletStore();
+
+ const isHydrated = useSyncExternalStore(
+ () => () => {},
+ () => true,
+ () => false,
+ );
+
+ const handleConnect = async () => {
+ setConnecting(true);
+ setError(null);
+
+ try {
+ const address = await connectWallet();
+
+ // SIWS Flow (Sign-In With Stellar)
+ // 1. Generate nonce/message (mocked here, usually from backend)
+ const message = `Sign in to Lance\n\nDomain: lance.so\nAddress: ${address}\nNonce: ${Math.random().toString(36).substring(2)}`;
+
+ // 2. Sign message
+ await signAuthMessage(message);
+
+ // 3. Check Network
+ const kit = getWalletsKit();
+ const connectedNetwork = await kit.getNetwork();
+ const appNetwork = (process.env.NEXT_PUBLIC_STELLAR_NETWORK as string) ?? "TESTNET";
+
+ if (connectedNetwork.network.toUpperCase() !== appNetwork.toUpperCase()) {
+ toast.warning(`Network mismatch: Wallet is on ${connectedNetwork.network}, but app is on ${appNetwork}`);
+ }
+
+ const walletId = kit.selectedWalletId || "freighter";
+ setConnection(address, walletId);
+ toast.success("Wallet connected successfully");
+ } catch (err: unknown) {
+ const errorMessage = err instanceof Error ? err.message : "Failed to connect wallet";
+ setError(errorMessage);
+ toast.error(errorMessage);
+ } finally {
+ setConnecting(false);
+ }
+ };
+
+ if (!isHydrated) return null;
+
+ if (isConnected && publicKey) {
+ return (
+
+
+
+
+
+
+ {publicKey.slice(0, 4)}...{publicKey.slice(-4)}
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {error && (
+
+ )}
+
+ );
+}
diff --git a/apps/web/components/navigation/top-nav.tsx b/apps/web/components/navigation/top-nav.tsx
index ee220682..2ddc2885 100644
--- a/apps/web/components/navigation/top-nav.tsx
+++ b/apps/web/components/navigation/top-nav.tsx
@@ -1,19 +1,16 @@
"use client";
-import { useEffect } from "react";
import Link from "next/link";
import { NetworkMismatchBanner } from "@/components/ui/network-mismatch-banner";
import { useAuthStore } from "@/lib/store/use-auth-store";
-import { useWalletAuth } from "@/hooks/use-wallet-auth";
import { Button } from "@/components/ui/button";
import {
- Search,
- Menu,
- LogOut,
- BriefcaseBusiness,
+ Bell,
LoaderCircle,
+ LogOut,
+ Menu,
+ Search,
TriangleAlert,
- Wallet,
Unplug,
} from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -22,39 +19,27 @@ import { SessionSwitcher } from "@/components/auth/session-switcher";
import { ThemeToggle } from "@/components/theme/theme-toggle";
import { BlockchainSyncIndicator } from "@/components/ui/blockchain-sync-indicator";
import { useWalletSession } from "@/hooks/use-wallet-session";
-import { toast } from "@/lib/toast";
import { WalletConnect } from "@/components/wallet/wallet-connect";
-import { ConnectWalletButton } from "@/components/wallet/connect-wallet-button";
-import { NotificationCenter } from "@/components/notifications/notification-center";
function shortAddress(address: string): string {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
}
export function TopNav({ onOpenSidebar }: { onOpenSidebar?: () => void }) {
- const { isLoggedIn, login, role, user, walletAddress } = useAuthStore();
- const { disconnect: disconnectAuth } = useWalletAuth();
+ const { isLoggedIn, logout, role, user } = useAuthStore();
const {
address,
- xlmBalance,
appNetwork,
walletNetwork,
- networkMismatch,
+ xlmBalance,
isConnected,
isConnecting,
+ networkMismatch,
error,
connect,
disconnect: disconnectSession,
} = useWalletSession();
- useEffect(() => {
- if (!error) return;
- toast.error({
- title: "Wallet error",
- description: error,
- });
- }, [error]);
-
return (
@@ -184,72 +169,38 @@ export function TopNav({ onOpenSidebar }: { onOpenSidebar?: () => void }) {
-
- {isLoggedIn ? (
-
-
-
-
-
- {user?.name
- ?.split(" ")
- .map((part) => part[0])
- .join("")
- .slice(0, 2) ?? "LN"}
-
-
-
-
{user?.name}
- {walletAddress ? (
-
-
- {walletAddress.slice(0, 4)}...{walletAddress.slice(-4)}
-
- ) : (
+
+
+
+ {isLoggedIn ? (
+
+
+
+
+
+
+ {user?.name
+ ?.split(" ")
+ .map((part) => part[0])
+ .join("")
+ .slice(0, 2) ?? "LN"}
+
+
+
+
{user?.name}
{user?.email}
- )}
+
+
-
-
-
- ) : (
-
-
-
-
-
- )}
+ ) : null}
+
diff --git a/apps/web/components/transaction/TxDetailsCard.tsx b/apps/web/components/transaction/TxDetailsCard.tsx
index be27b388..5ceabbed 100644
--- a/apps/web/components/transaction/TxDetailsCard.tsx
+++ b/apps/web/components/transaction/TxDetailsCard.tsx
@@ -46,6 +46,7 @@ export function TxDetailsCard({
const txHash = metadata.txHash;
const unsignedXdr = devData?.unsignedXdr;
const fee = submission?.hash ? "100" : undefined; // Placeholder fee
+ const ledger = event.polling?.ledger;
const explorerUrl =
network === "public"
@@ -144,7 +145,7 @@ export function TxDetailsCard({
)}
{/* Ledger Sequence */}
- {event.polling?.ledger && (
+ {ledger && (
@@ -155,10 +156,10 @@ export function TxDetailsCard({
- {event.polling.ledger}
+ {ledger}