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 && ( +
+ + {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 ? ( -

-

- ) : ( +
+ + + {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}