From 0077ea1489d7c40e73338346095dee9672b6bfed Mon Sep 17 00:00:00 2001 From: emdevelopa Date: Tue, 24 Feb 2026 03:02:21 +0000 Subject: [PATCH 1/6] feat: added #129 Epic: Production ready wallet integrations --- frontend/app/globals.css | 272 ++++++++++++++++++ frontend/components/Navbar.tsx | 5 +- .../components/dashboard/dashboard-view.tsx | 169 ++++++----- frontend/components/wallet/WalletButton.tsx | 163 +++++++++++ frontend/components/wallet/WalletModal.tsx | 201 +++++++++++++ frontend/context/wallet-context.tsx | 4 +- frontend/lib/dashboard.ts | 29 -- frontend/lib/wallet.ts | 199 +++++++------ frontend/package.json | 6 +- 9 files changed, 859 insertions(+), 189 deletions(-) create mode 100644 frontend/components/wallet/WalletButton.tsx create mode 100644 frontend/components/wallet/WalletModal.tsx diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 904355d..363546c 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -969,3 +969,275 @@ body { top: 1rem; } } + +/* ── Wallet Modal ─────────────────────────────────────────────────────────── */ + +.wallet-modal-backdrop { + position: fixed; + inset: 0; + z-index: 200; + background: rgba(2, 6, 23, 0.72); + backdrop-filter: blur(6px); + display: grid; + place-items: center; + padding: 1rem; + animation: fade-in 180ms ease-out; +} + +.wallet-modal { + width: min(860px, 100%); + border-radius: 1.55rem; + border: 1px solid var(--surface-border); + background: var(--surface); + box-shadow: 0 30px 70px rgba(13, 48, 90, 0.22); + backdrop-filter: blur(12px); + padding: clamp(1.25rem, 2.8vw, 2.5rem); + animation: panel-rise 400ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.wallet-modal__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.wallet-modal__header h2 { + margin: 0.5rem 0 0; + font-size: clamp(1.4rem, 2.5vw, 2rem); + line-height: 1.12; +} + +.wallet-modal__close { + flex-shrink: 0; + width: 2.2rem; + height: 2.2rem; + border: 1px solid rgba(19, 38, 61, 0.14); + border-radius: 0.6rem; + background: rgba(255, 255, 255, 0.7); + color: var(--text-muted); + font-size: 0.95rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 160ms; +} + +.wallet-modal__close:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.95); + color: var(--text-main); +} + +.wallet-modal__close:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +/* Wallet card notes */ +.wallet-card__note { + margin: -0.35rem 0 0; + font-size: 0.8rem !important; + color: var(--accent-strong) !important; + font-style: italic; +} + +/* Install link variant of the wallet button */ +.wallet-button--install { + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + background: linear-gradient(90deg, #1a6480, var(--wallet-accent)); + font-size: 0.9rem; +} + +/* Spinner row inside connecting button */ +.wallet-button__spinner-row { + display: flex; + align-items: center; + justify-content: center; + gap: 0.55rem; +} + +.wallet-button__spinner { + width: 0.82rem; + height: 0.82rem; + border-radius: 999px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: #fff; + animation: spin 700ms linear infinite; + display: inline-block; +} + +/* Card unavailable state (e.g. Freighter not installed) */ +.wallet-card[data-unavailable="true"] { + opacity: 0.72; +} + +/* ── WalletButton ─────────────────────────────────────────────────────────── */ + +.wallet-connect-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 2.4rem; + padding: 0 1.15rem; + border: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--accent-strong), var(--wallet-accent)); + color: #fff; + font-size: 0.92rem; + font-weight: 600; + cursor: pointer; + transition: filter 180ms ease, transform 140ms ease, box-shadow 180ms ease; + box-shadow: 0 4px 14px rgba(15, 122, 153, 0.28); +} + +.wallet-connect-btn:hover { + filter: brightness(1.08); + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(15, 122, 153, 0.35); +} + +.wallet-connect-btn:active { + transform: translateY(0); +} + +.wallet-btn-skeleton { + width: 140px; + height: 2.4rem; + border-radius: 999px; + background: rgba(19, 38, 61, 0.1); + animation: pulse 920ms ease-in-out infinite alternate; +} + +.wallet-btn-connecting { + display: inline-flex; + align-items: center; + gap: 0.55rem; + height: 2.4rem; + padding: 0 1rem; + border-radius: 999px; + background: rgba(15, 122, 153, 0.14); + color: var(--accent-strong); + font-size: 0.88rem; + font-weight: 600; +} + +.wallet-status-spinner { + width: 0.82rem; + height: 0.82rem; + border-radius: 999px; + border: 2px solid rgba(13, 83, 120, 0.22); + border-top-color: var(--accent-strong); + animation: spin 700ms linear infinite; +} + +/* Connected chip wrapper (positions dropdown) */ +.wallet-chip-wrapper { + position: relative; +} + +/* Network mismatch badge state */ +.wallet-chip__network[data-mismatch="true"] { + background: rgba(177, 47, 63, 0.14); + color: var(--danger); +} + +/* ── Wallet Dropdown ──────────────────────────────────────────────────────── */ + +.wallet-dropdown { + position: absolute; + top: calc(100% + 0.5rem); + right: 0; + min-width: 240px; + border-radius: 1rem; + border: 1px solid rgba(19, 38, 61, 0.14); + background: rgba(255, 255, 255, 0.96); + box-shadow: 0 14px 36px rgba(13, 48, 90, 0.18); + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.55rem; + animation: slide-down 180ms ease-out; + z-index: 201; +} + +.wallet-dropdown__key { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + border: 1px solid rgba(19, 38, 61, 0.1); + border-radius: 0.65rem; + padding: 0.55rem 0.7rem; + background: rgba(255, 255, 255, 0.6); +} + +.wallet-dropdown__key code { + font-family: var(--font-mono), "IBM Plex Mono", monospace; + font-size: 0.8rem; + color: #183355; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 160px; +} + +.wallet-dropdown__copy { + flex-shrink: 0; + border: 0; + background: transparent; + color: var(--accent-strong); + font-size: 0.8rem; + font-weight: 600; + cursor: pointer; + padding: 0.15rem 0.3rem; + border-radius: 0.4rem; + transition: background 140ms; +} + +.wallet-dropdown__copy:hover { + background: rgba(13, 83, 120, 0.1); +} + +.wallet-dropdown__warning { + margin: 0; + font-size: 0.8rem; + color: #8c2230; + background: rgba(255, 243, 245, 0.9); + border: 1px solid rgba(177, 47, 63, 0.24); + border-radius: 0.65rem; + padding: 0.45rem 0.6rem; + line-height: 1.4; +} + +.wallet-dropdown__disconnect { + border: 1px solid rgba(19, 38, 61, 0.16); + border-radius: 0.7rem; + height: 2.1rem; + background: rgba(255, 255, 255, 0.8); + color: var(--danger); + font-size: 0.88rem; + font-weight: 600; + cursor: pointer; + transition: background 140ms; +} + +.wallet-dropdown__disconnect:hover { + background: rgba(177, 47, 63, 0.06); +} + +@media (max-width: 640px) { + .wallet-dropdown { + right: auto; + left: 0; + } + + .wallet-modal { + border-radius: 1.15rem; + } +} + diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index 154ceee..471ad14 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Button } from './ui/Button'; import { ModeToggle } from './ModeToggle'; +import { WalletButton } from './wallet/WalletButton'; export const Navbar = () => { return ( @@ -24,9 +25,7 @@ export const Navbar = () => {
- +
); diff --git a/frontend/components/dashboard/dashboard-view.tsx b/frontend/components/dashboard/dashboard-view.tsx index fceae1b..6152355 100644 --- a/frontend/components/dashboard/dashboard-view.tsx +++ b/frontend/components/dashboard/dashboard-view.tsx @@ -1,14 +1,31 @@ "use client"; -import React from "react"; +/** + * components/dashboard/dashboard-view.tsx + * + * Changes from previous version: + * - Removed "Mocked wallet session" warning banner (no longer relevant). + * - wallet-chip now shows network badge alongside the public key. + * - formatNetwork() used so "PUBLIC" → "Mainnet", "TESTNET" → "Testnet". + */ + +import React from "react"; import { getDashboardAnalytics, getMockDashboardStats, type DashboardSnapshot, } from "@/lib/dashboard"; -import { shortenPublicKey, type WalletSession } from "@/lib/wallet"; +import { + shortenPublicKey, + formatNetwork, + isExpectedNetwork, + type WalletSession, +} from "@/lib/wallet"; import IncomingStreams from "../IncomingStreams"; -import { StreamCreationWizard, type StreamFormData } from "../stream-creation/StreamCreationWizard"; +import { + StreamCreationWizard, + type StreamFormData, +} from "../stream-creation/StreamCreationWizard"; import { Button } from "../ui/Button"; interface DashboardViewProps { @@ -42,10 +59,7 @@ function formatAnalyticsValue( value: number, format: "currency" | "percent", ): string { - if (format === "currency") { - return formatCurrency(value); - } - + if (format === "currency") return formatCurrency(value); return new Intl.NumberFormat("en-US", { style: "percent", maximumFractionDigits: 1, @@ -54,11 +68,7 @@ function formatAnalyticsValue( function formatActivityTime(timestamp: string): string { const date = new Date(timestamp); - - if (Number.isNaN(date.getTime())) { - return timestamp; - } - + if (Number.isNaN(date.getTime())) return timestamp; return new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "short", @@ -67,18 +77,18 @@ function formatActivityTime(timestamp: string): string { function renderAnalytics(snapshot: DashboardSnapshot | null) { const metrics = getDashboardAnalytics(snapshot); - return ( -
+

Analytics Overview

Computed from wallet activity
-
{metrics.map((metric) => { const isUnavailable = metric.value === null; - return (
{isUnavailable ? "No data" - : formatAnalyticsValue(metric.value, metric.format)} + : formatAnalyticsValue(metric.value!, metric.format)} - {isUnavailable ? metric.unavailableText : metric.detail} + + {isUnavailable ? metric.unavailableText : metric.detail} +
); })} @@ -151,7 +163,6 @@ function renderStreams(

My Active Streams

{snapshot.streams.length} total
-
@@ -201,14 +212,12 @@ function renderRecentActivity(snapshot: DashboardSnapshot) {

Recent Activity

{snapshot.recentActivity.length} items - {snapshot.recentActivity.length > 0 ? (
    {snapshot.recentActivity.map((activity) => { const amountPrefix = activity.direction === "received" ? "+" : "-"; const amountClass = activity.direction === "received" ? "is-positive" : "is-negative"; - return (
  • @@ -233,6 +242,8 @@ function renderRecentActivity(snapshot: DashboardSnapshot) { ); } +// ── Main component ──────────────────────────────────────────────────────────── + export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const [activeTab, setActiveTab] = React.useState("overview"); const [showWizard, setShowWizard] = React.useState(false); @@ -250,63 +261,65 @@ export function DashboardView({ session, onDisconnect }: DashboardViewProps) { const handleCreateStream = async (data: StreamFormData) => { console.log("Creating stream with data:", data); // TODO: Integrate with Soroban contract's create_stream function - // This would involve: - // 1. Converting duration to seconds - // 2. Calling the contract's create_stream function - // 3. Handling the transaction signing - // 4. Waiting for confirmation - - // For now, simulate success await new Promise((resolve) => setTimeout(resolve, 1500)); - alert(`Stream created successfully!\n\nRecipient: ${data.recipient}\nToken: ${data.token}\nAmount: ${data.amount}\nDuration: ${data.duration} ${data.durationUnit}`); + alert( + `Stream created successfully!\n\nRecipient: ${data.recipient}\nToken: ${data.token}\nAmount: ${data.amount}\nDuration: ${data.duration} ${data.durationUnit}`, + ); setShowWizard(false); }; const renderContent = () => { if (activeTab === "incoming") { - return
    ; + return ( +
    + +
    + ); } if (activeTab === "overview") { - if (!stats) { - return ( -
    -

    No stream data yet

    -

    - Your account is connected, but there are no active or historical - stream records available yet. -

    -
      -
    • Create your first payment stream
    • -
    • Invite a recipient to start receiving funds
    • -
    • Check back once transactions are confirmed
    • -
    -
    - -
    -
    - ); - } + if (!stats) { return ( -
    - {renderStats(stats)} - {renderAnalytics(stats)} - {renderStreams(stats, handleTopUp)} - {renderRecentActivity(stats)} +
    +

    No stream data yet

    +

    + Your account is connected, but there are no active or historical + stream records available yet. +

    +
      +
    • Create your first payment stream
    • +
    • Invite a recipient to start receiving funds
    • +
    • Check back once transactions are confirmed
    • +
    +
    +
    +
    ); + } + return ( +
    + {renderStats(stats)} + {renderAnalytics(stats)} + {renderStreams(stats, handleTopUp)} + {renderRecentActivity(stats)} +
    + ); } - + return ( -
    -

    Under Construction

    -

    This tab is currently under development.

    -
    +
    +

    Under Construction

    +

    This tab is currently under development.

    +
    ); }; + const networkLabel = formatNetwork(session.network); + const networkOk = isExpectedNetwork(session.network); + return (