From 4512ef1d8a1dd31a8f0a0916ffb0c8b0021bf831 Mon Sep 17 00:00:00 2001 From: NasSharaf Date: Thu, 12 Jun 2025 16:16:08 -0400 Subject: [PATCH] Connected to backend and fixed up UI changes, created a test-connection page --- .../ps/[id]/(deployments)/dep/page.tsx | 160 +++- .../ps/[id]/(integrations)/int/GitPage.tsx | 52 +- .../env/EnvVarsPage.tsx | 76 +- .../(settings)/set/ProjectSettingsPage.tsx | 343 ++++--- .../ps/[id]/(settings)/set/page.tsx | 93 +- .../projects/[provider]/ps/[id]/page.tsx | 819 +++++++++++++--- .../(dashboard)/projects/page.tsx | 180 ++-- .../app/auth/github/backend-callback/page.tsx | 166 ++++ apps/deploy-fe/src/app/layout.tsx | 4 + .../src/app/test-connection/page.tsx | 882 ++++++++++++++++++ apps/deploy-fe/src/components/AuthTest.tsx | 411 ++++++++ .../src/components/DeploymentTest.tsx | 171 ++++ .../src/components/DirectKeyAuth.tsx | 160 ++++ apps/deploy-fe/src/components/GQLTest.tsx | 93 ++ .../src/components/GitHubBackendAuth.tsx | 241 +++++ .../main-navigation/MainNavigation.tsx | 5 +- .../WalletSessionBadge.tsx | 53 +- .../wallet-session-id/WalletSessionId.tsx | 21 +- .../auto-sign-in/AutoSignInIFrameModal.tsx | 416 +++++++-- .../CheckBalanceIframe.tsx | 71 +- .../configure-step/configure-step.tsx | 554 +++++++---- .../onboarding/connect-step/connect-step.tsx | 456 +++++++-- .../onboarding/deploy-step/deploy-step.tsx | 394 +++++--- .../onboarding/success-step/success-step.tsx | 51 +- .../project/ProjectCard/FixedProjectCard.tsx | 37 +- apps/deploy-fe/src/components/providers.tsx | 31 +- .../src/components/templates/TemplateCard.tsx | 114 +++ .../src/components/templates/TemplateIcon.tsx | 37 + .../templates/TemplateSelection.tsx | 109 +++ apps/deploy-fe/src/constants/templates.tsx | 46 + apps/deploy-fe/src/context/BackendContext.tsx | 111 +++ apps/deploy-fe/src/context/OctokitContext.tsx | 58 +- apps/deploy-fe/src/context/WalletContext.tsx | 189 +--- .../src/context/WalletContextProvider.tsx | 415 ++++---- .../src/hooks/disabled_useRepoData.tsx | 169 ---- apps/deploy-fe/src/hooks/useAuthStatus.tsx | 215 +++++ apps/deploy-fe/src/hooks/useDeployment.tsx | 143 +-- apps/deploy-fe/src/hooks/useRepoData.tsx | 7 - apps/deploy-fe/src/hooks/useTemplate.tsx | 186 ++++ package.json | 4 + pnpm-lock.yaml | 90 +- 41 files changed, 6172 insertions(+), 1661 deletions(-) create mode 100644 apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx create mode 100644 apps/deploy-fe/src/app/test-connection/page.tsx create mode 100644 apps/deploy-fe/src/components/AuthTest.tsx create mode 100644 apps/deploy-fe/src/components/DeploymentTest.tsx create mode 100644 apps/deploy-fe/src/components/DirectKeyAuth.tsx create mode 100644 apps/deploy-fe/src/components/GQLTest.tsx create mode 100644 apps/deploy-fe/src/components/GitHubBackendAuth.tsx create mode 100644 apps/deploy-fe/src/components/templates/TemplateCard.tsx create mode 100644 apps/deploy-fe/src/components/templates/TemplateIcon.tsx create mode 100644 apps/deploy-fe/src/components/templates/TemplateSelection.tsx create mode 100644 apps/deploy-fe/src/constants/templates.tsx create mode 100644 apps/deploy-fe/src/context/BackendContext.tsx delete mode 100644 apps/deploy-fe/src/hooks/disabled_useRepoData.tsx create mode 100644 apps/deploy-fe/src/hooks/useAuthStatus.tsx create mode 100644 apps/deploy-fe/src/hooks/useTemplate.tsx diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(deployments)/dep/page.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(deployments)/dep/page.tsx index 5c97da1..4aeb380 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(deployments)/dep/page.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(deployments)/dep/page.tsx @@ -3,14 +3,15 @@ import { useParams } from 'next/navigation'; import { PageWrapper } from '@/components/foundation'; import { DeploymentDetailsCard } from '@/components/projects/project/deployments/DeploymentDetailsCard'; -import { FilterForm } from '@/components/projects/project/deployments/FilterForm'; import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; -import { IconButton } from '@workspace/ui/components/button'; -import { Rocket } from 'lucide-react'; +import { Button } from '@workspace/ui/components/button'; +import { Square, Search, Calendar, ChevronDown } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useRepoData } from '@/hooks/useRepoData'; import type { Deployment, Domain } from '@/types'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@workspace/ui/components/dialog'; +import { Input } from '@workspace/ui/components/input'; export default function DeploymentsPage() { const router = useRouter(); @@ -26,6 +27,11 @@ export default function DeploymentsPage() { const [deployments, setDeployments] = useState([]); const [filteredDeployments, setFilteredDeployments] = useState([]); const [prodBranchDomains, setProdBranchDomains] = useState([]); + + // State for deployment logs modal + const [isLogsOpen, setIsLogsOpen] = useState(false); + const [selectedDeploymentId, setSelectedDeploymentId] = useState(null); + const [deploymentLogs, setDeploymentLogs] = useState(''); // Create a default deployment const defaultDeployment: Deployment = { @@ -56,11 +62,17 @@ export default function DeploymentsPage() { } }; - // Initialize with mock data + // Initialize with empty data for testing the empty state + // Comment this out to see the mock deployments useEffect(() => { - const mockDeployments = [defaultDeployment, secondDeployment]; - setDeployments(mockDeployments); - setFilteredDeployments(mockDeployments); + // For testing the empty state + setDeployments([]); + setFilteredDeployments([]); + + // Uncomment to see mock deployments + // const mockDeployments = [defaultDeployment, secondDeployment]; + // setDeployments(mockDeployments); + // setFilteredDeployments(mockDeployments); // Mock domains const mockDomains: Domain[] = [ @@ -102,6 +114,31 @@ export default function DeploymentsPage() { setFilteredDeployments(deployments); }; + // View logs handler + const handleViewLogs = (deploymentId: string) => { + setSelectedDeploymentId(deploymentId); + + // Mock logs data + const mockLogs = `[2025-02-12 10:03:12] INFO Starting deployment process for service: api-gateway +[2025-02-12 10:03:14] INFO Fetching latest commit from main branch (commit: a1b2c3d) +[2025-02-12 10:03:15] INFO Building Docker image: registry.company.com/api-gateway:latest +[2025-02-12 10:03:26] INFO Running security scan on built image +[2025-02-12 10:03:27] WARNING Medium severity vulnerability detected in package 'openssl' +[2025-02-12 10:03:30] INFO Pushing image to container registry +[2025-02-12 10:03:35] INFO Updating Kubernetes deployment +[2025-02-12 10:03:40] INFO Scaling down old pods +[2025-02-12 10:03:42] INFO Scaling up new pods +[2025-02-12 10:03:50] INFO Running health checks on new pods +[2025-02-12 10:03:52] ERROR Pod 'api-gateway-7df9bbb500-tx2k4' failed readiness probe (502 Bad Gateway) +[2025-02-12 10:03:55] INFO Retrying deployment with previous stable image +[2025-02-12 10:04:03] INFO Rolling back to registry.company.com/api-gateway:previous +[2025-02-12 10:04:10] INFO Deployment rolled back successfully +[2025-02-12 10:04:11] ERROR Deployment failed, please review logs and fix errors`; + + setDeploymentLogs(mockLogs); + setIsLogsOpen(true); + }; + const project = { id: id, prodBranch: 'main', @@ -110,6 +147,8 @@ export default function DeploymentsPage() { const currentDeployment = deployments.find(deployment => deployment.isCurrent) || defaultDeployment; + const hasDeployments = deployments.length > 0; + return (
- + {/* Filter Controls - Always visible but disabled when no deployments */} +
+ {/* Search box */} +
+
+ +
+ +
+ + {/* Date selector */} +
+ +
+ + {/* Status dropdown */} +
+ +
+
+
{filteredDeployments.length > 0 ? ( filteredDeployments.map((deployment) => ( - +
+ +
+ +
+
)) ) : ( -
-
-

- No deployments found -

-

- Please change your search query or filters. -

+ // Updated empty state to match screenshot +
+
+
- You have no deployments +

+ Please change your search query or filters. +

+
)}
+ + {/* Deployment Logs Modal */} + + + + Deployment Logs + +
+
+              {deploymentLogs}
+            
+
+
+ +
+
+
); } \ No newline at end of file diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/GitPage.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/GitPage.tsx index 9c03b46..5d745db 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/GitPage.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(integrations)/int/GitPage.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { useRouter, useParams } from "next/navigation"; import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay"; interface SwitchProps { @@ -26,12 +25,12 @@ function Switch({ id, checked, onChange, disabled = false }: SwitchProps) { disabled={disabled} />
@@ -39,9 +38,6 @@ function Switch({ id, checked, onChange, disabled = false }: SwitchProps) { } export default function GitPage() { - const params = useParams(); - const { provider, id } = params; - const [pullRequestComments, setPullRequestComments] = useState(true); const [commitComments, setCommitComments] = useState(false); const [productionBranch, setProductionBranch] = useState("main"); @@ -88,8 +84,8 @@ export default function GitPage() { {(isSavingBranch || isSavingWebhook) && }
-
-

Git repository

+
+

Git repository

@@ -100,11 +96,11 @@ export default function GitPage() { checked={pullRequestComments} onChange={setPullRequestComments} /> -
-

+

Laconic will comment on pull requests opened against this project.

@@ -118,11 +114,11 @@ export default function GitPage() { checked={commitComments} onChange={setCommitComments} /> -
-

+

Laconic will comment on commits deployed to production.

@@ -130,47 +126,47 @@ export default function GitPage() {
-
-

Production branch

+
+

Production branch

-

+

By default, each commit pushed to the main branch initiates a production deployment. You can opt for a different branch for deployment in the settings.

-
-
-

Deploy webhooks

+
+

Deploy webhooks

-

+

Webhooks configured to trigger when there is a change in a project's build or deployment status.

-
diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx index 67718a3..3416302 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/(environment-variables)/env/EnvVarsPage.tsx @@ -1,7 +1,6 @@ "use client"; import { useState } from "react"; -import { useRouter, useParams } from "next/navigation"; import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay"; import { PlusIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon, TrashIcon } from "lucide-react"; @@ -21,16 +20,16 @@ interface EnvGroupProps { function EnvGroup({ title, isOpen, onToggle, children, varCount }: EnvGroupProps) { return ( -
+
-

{title}

- ({varCount}) +

{title}

+ ({varCount})
-
@@ -44,9 +43,6 @@ function EnvGroup({ title, isOpen, onToggle, children, varCount }: EnvGroupProps } export default function EnvVarsPage() { - const params = useParams(); - const { provider, id } = params; - const [isAddingVar, setIsAddingVar] = useState(false); const [newVarKey, setNewVarKey] = useState(""); const [newVarValue, setNewVarValue] = useState(""); @@ -175,7 +171,7 @@ export default function EnvVarsPage() { return (
{ const updatedVars = env === 'production' @@ -191,7 +187,7 @@ export default function EnvVarsPage() { placeholder="KEY" /> { const updatedVars = env === 'production' @@ -207,13 +203,13 @@ export default function EnvVarsPage() { placeholder="Value" /> ) : ( -
+
- + setNewVarKey(e.target.value)} - className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white" + className="w-full px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring" placeholder="KEY" />
- + setNewVarValue(e.target.value)} - className="w-full px-3 py-2 rounded-md bg-gray-900 border border-gray-700 text-white" + className="w-full px-3 py-2 rounded-md bg-background border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring" placeholder="Value" />
- +
handleEnvSelectionChange('production')} - className="mr-2" + className="mr-2 rounded border-border" /> - +
handleEnvSelectionChange('preview')} - className="mr-2" + className="mr-2 rounded border-border" /> - +
handleEnvSelectionChange('development')} - className="mr-2" + className="mr-2 rounded border-border" /> - +
) : ( -

No variables defined

+

No variables defined

)} @@ -379,7 +375,7 @@ export default function EnvVarsPage() { {previewVars.map((variable, index) => renderEnvVarRow('preview', variable, index))}
) : ( -

No variables defined

+

No variables defined

)} @@ -394,18 +390,18 @@ export default function EnvVarsPage() { {deploymentVars.map((variable, index) => renderEnvVarRow('development', variable, index))}
) : ( -

No variables defined

+

No variables defined

)}
diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/ProjectSettingsPage.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/ProjectSettingsPage.tsx index 74f8b56..8b78ec9 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/ProjectSettingsPage.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/ProjectSettingsPage.tsx @@ -1,47 +1,26 @@ "use client"; import { useState, useEffect } from "react"; -import { useRouter, useParams } from "next/navigation"; +import { useSearchParams, useRouter } from "next/navigation"; import { Clipboard } from "lucide-react"; import { Dropdown } from "@/components/core/dropdown"; import { LoadingOverlay } from "@/components/foundation/loading/loading-overlay"; -import { useRepoData } from "@/hooks/useRepoData"; +import { useGQLClient } from "@/context"; +import type { Project } from '@workspace/gql-client'; +import { Button } from '@workspace/ui/components/button'; +import { Input } from '@workspace/ui/components/input'; +import { Label } from '@workspace/ui/components/label'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@workspace/ui/components/dialog'; -// Create a simple modal component -interface ModalProps { - isOpen: boolean; - onClose: () => void; - title: string; - children: React.ReactNode; - footer?: React.ReactNode; +interface ProjectSettingsPageProps { + project?: Project; + onProjectUpdated?: () => void; } -function Modal({ isOpen, onClose, title, children, footer }: ModalProps) { - if (!isOpen) return null; - - return ( -
-
-
-

{title}

- -
-
{children}
- {footer &&
{footer}
} -
-
- ); -} - -export default function ProjectSettingsPage() { +export default function ProjectSettingsPage({ project, onProjectUpdated }: ProjectSettingsPageProps) { + const client = useGQLClient(); const router = useRouter(); - const params = useParams(); - const id = params?.id ? String(params.id) : ''; - - // Use the hook to get repo data - const { repoData, isLoading } = useRepoData(id); + const searchParams = useSearchParams(); const [projectName, setProjectName] = useState(""); const [projectDescription, setProjectDescription] = useState(""); @@ -51,70 +30,111 @@ export default function ProjectSettingsPage() { const [isTransferring, setIsTransferring] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); // Update form values when project data is loaded useEffect(() => { - if (repoData) { - setProjectName(repoData.name || ""); - setProjectDescription(repoData.description || ""); - setProjectId(repoData.id?.toString() || ""); + if (project) { + setProjectName(project.name || ""); + setProjectDescription(project.description || ""); + setProjectId(project.id || ""); } - }, [repoData]); + }, [project]); + + // Check for delete action in URL params + useEffect(() => { + const action = searchParams?.get('action'); + if (action === 'delete' && project) { + setIsDeleteModalOpen(true); + // Clean up the URL by removing the query parameter + const url = new URL(window.location.href); + url.searchParams.delete('action'); + router.replace(url.pathname); + } + }, [searchParams, project, router]); const accountOptions = [ { label: "Personal Account", value: "account1" }, { label: "Team Account", value: "account2" } ]; + const showMessage = (message: string, isError = false) => { + if (isError) { + setErrorMessage(message); + setSuccessMessage(""); + } else { + setSuccessMessage(message); + setErrorMessage(""); + } + setTimeout(() => { + setSuccessMessage(""); + setErrorMessage(""); + }, 3000); + }; + const handleSave = async () => { + if (!project) return; + try { setIsSaving(true); - console.log("Saving project info:", { projectName, projectDescription }); - // Simulate API call - await new Promise(resolve => setTimeout(resolve, 1000)); + await client.updateProject(project.id, { + name: projectName, + description: projectDescription + }); - // Show success notification - in a real app you'd use a toast library - console.log("Project updated successfully"); + showMessage("Project updated successfully"); + if (onProjectUpdated) { + onProjectUpdated(); + } } catch (error) { console.error("Failed to save project info:", error); - // Show error notification + showMessage("Failed to update project", true); } finally { setIsSaving(false); } }; const handleTransfer = async () => { + if (!project) return; + try { setIsTransferring(true); - // Transfer project to selected account + + // TODO: Implement actual transfer API call when available console.log("Transferring project to:", selectedAccount); - // Implement API call to transfer project await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call - // After successful transfer, navigate back to projects list - router.push("/dashboard/projects"); + showMessage("Project transfer initiated successfully"); + // Note: No navigation - staying in the same tab } catch (error) { console.error("Failed to transfer project:", error); - // Show error notification + showMessage("Failed to transfer project", true); } finally { setIsTransferring(false); } }; const handleDelete = async () => { + if (!project) return; + try { setIsDeleting(true); - // Delete project - console.log("Deleting project"); - // Implement API call to delete project - await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate API call - // After successful deletion, navigate back to projects list - router.push("/dashboard/projects"); + await client.deleteProject(project.id); + + showMessage("Project deleted successfully"); + setIsDeleteModalOpen(false); + + // Navigate back to projects list after successful deletion + setTimeout(() => { + router.push('/projects'); + }, 1500); + } catch (error) { console.error("Failed to delete project:", error); - // Show error notification + showMessage("Failed to delete project", true); } finally { setIsDeleting(false); setIsDeleteModalOpen(false); @@ -123,29 +143,15 @@ export default function ProjectSettingsPage() { const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); + showMessage("Project ID copied to clipboard"); }; - const DeleteModalFooter = ( -
- - -
- ); - - if (isLoading) { - return ; + if (!project) { + return ( +
+ No project data available +
+ ); } return ( @@ -153,47 +159,61 @@ export default function ProjectSettingsPage() { {(isSaving || isTransferring || isDeleting) && }
-
-

Project Info

+ {/* Success/Error Messages */} + {successMessage && ( +
+ {successMessage} +
+ )} + {errorMessage && ( +
+ {errorMessage} +
+ )} + + {/* Project Info Section */} +
+

Project Info

-
-
-
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
-
-

Transfer Project

+ {/* Transfer Project Section */} +
+

Transfer Project

-
- - setSelectedAccount(value)} - className="w-full" - /> +
+
+ + setSelectedAccount(value)} + className="w-full mt-1" + /> +
-

+

Transfer this app to your personal account or a team you are a member of.

- +
-
-

Delete Project

+ {/* Delete Project Section */} +
+

Delete Project

-

+

The project will be permanently deleted, including its deployments and domains. This action is irreversible and cannot be undone.

- - - !isDeleting && setIsDeleteModalOpen(false)} - title="Are you absolutely sure?" - footer={DeleteModalFooter} - > -

- This action cannot be undone. This will permanently delete the project - and all associated deployments and domains. -

-
+
+ + {/* Delete Confirmation Modal */} + !isDeleting && setIsDeleteModalOpen(open)}> + + + Are you absolutely sure? + +
+

+ This action cannot be undone. This will permanently delete the project{" "} + "{project.name}" and all associated deployments and domains. +

+
+ + + + +
+
); } \ No newline at end of file diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/page.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/page.tsx index 71b7a78..52e70f1 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/page.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/(settings)/set/page.tsx @@ -1,21 +1,56 @@ 'use client'; import { useParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; import { PageWrapper } from "@/components/foundation"; import { Tabs, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; import ProjectSettingsPage from "./ProjectSettingsPage"; import { useRouter } from 'next/navigation'; -import { useRepoData } from '@/hooks/useRepoData'; +import { useGQLClient } from '@/context'; +import type { Project } from '@workspace/gql-client'; export default function SettingsPage() { const router = useRouter(); const params = useParams(); + const client = useGQLClient(); + // Safely unwrap params const id = params?.id ? String(params.id) : ''; const provider = params?.provider ? String(params.provider) : ''; - // Use the hook to get repo data - const { repoData } = useRepoData(id); + // State for project data + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Load project data using GraphQL client + useEffect(() => { + if (id) { + loadProject(id); + } + }, [id]); + + const loadProject = async (projectId: string) => { + try { + setLoading(true); + setError(null); + + const response = await client.getProject(projectId); + setProject(response.project); + } catch (err) { + console.error('Failed to load project:', err); + setError(err instanceof Error ? err.message : 'Failed to load project'); + } finally { + setLoading(false); + } + }; + + // Handle project updates + const handleProjectUpdated = () => { + if (id) { + loadProject(id); + } + }; // Handle tab changes by navigating to the correct folder const handleTabChange = (value: string) => { @@ -39,21 +74,60 @@ export default function SettingsPage() { break; } }; + + // Show loading state + if (loading) { + return ( + +
+
Loading project settings...
+
+
+ ); + } + + // Show error state + if (error || !project) { + return ( + +
+
+
Failed to load project
+
{error}
+
+
+
+ ); + } return ( - {/* Settings content */} + {/* Settings content - now with proper project data */}
- +
diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/page.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/page.tsx index 20049ad..52895dd 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/page.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/[provider]/ps/[id]/page.tsx @@ -9,119 +9,325 @@ import { AvatarFallback} from '@workspace/ui/components/avatar'; import { Button } from '@workspace/ui/components/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs'; -import { Activity, Clock, GitBranch, ExternalLink } from 'lucide-react'; +import { Activity, Clock, GitBranch, ExternalLink, AlertCircle, Square, Search, Calendar, ChevronDown, Clipboard } from 'lucide-react'; +import { Badge } from '@workspace/ui/components/badge'; +import { Input } from '@workspace/ui/components/input'; +import { Label } from '@workspace/ui/components/label'; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@workspace/ui/components/dialog'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { useRepoData } from '@/hooks/useRepoData'; +import { useGQLClient } from '@/context'; import { useEffect, useState } from 'react'; +import type { Project } from '@workspace/gql-client'; + +// Import the tab content components +import GitPage from './(integrations)/int/GitPage'; +import EnvVarsPage from './(settings)/set/(environment-variables)/env/EnvVarsPage'; export default function ProjectOverviewPage() { const router = useRouter(); const params = useParams(); + const client = useGQLClient(); + // Safely unwrap params const id = params?.id ? String(params.id) : ''; - const provider = params?.provider ? String(params.provider) : ''; - // Use the hook to get repo data - const { repoData } = useRepoData(id); + // State management + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState('overview'); // Local tab state - // Default deployment details - const [deploymentUrl, setDeploymentUrl] = useState(''); - const [deploymentDate, setDeploymentDate] = useState(Date.now() - 60 * 60 * 1000); // 1 hour ago - const [deployedBy, setDeployedBy] = useState(''); - const [projectName, setProjectName] = useState(''); - const [branch, setBranch] = useState('main'); - - // Update details when repo data is loaded + // Deployment page state + const [deployments, setDeployments] = useState([]); + const [filteredDeployments, setFilteredDeployments] = useState([]); + const [isLogsOpen, setIsLogsOpen] = useState(false); + const [deploymentLogs, setDeploymentLogs] = useState(''); + + // Load project data useEffect(() => { - if (repoData) { - setProjectName(repoData.name); - setBranch(repoData.default_branch || 'main'); - setDeployedBy(repoData.owner?.login || 'username'); - // Create a deployment URL based on the repo name - setDeploymentUrl(`https://${repoData.name.toLowerCase()}.example.com`); + if (id) { + loadProject(id); } - }, [repoData]); - - // Auction data - const auctionId = 'laconic1sdfjwei4jfkasifgjiai45ioasjf5jjjafij355'; + }, [id]); - // Activities data - const activities = [ - { - username: deployedBy || 'username', - branch: branch, - action: 'deploy: source cargo', - time: '5 minutes ago' - }, - { - username: deployedBy || 'username', - branch: branch, - action: 'bump', - time: '5 minutes ago' - }, - { - username: deployedBy || 'username', - branch: branch, - action: 'version: update version', - time: '5 minutes ago' - }, - { - username: deployedBy || 'username', - branch: branch, - action: 'build: updates', - time: '5 minutes ago' - } - ]; - - // Handle tab changes by navigating to the correct folder - const handleTabChange = (value: string) => { - const basePath = `/projects/${provider}/ps/${id}`; - - switch (value) { - case 'overview': - router.push(basePath); - break; - case 'deployment': - router.push(`${basePath}/dep`); - break; - case 'settings': - router.push(`${basePath}/set`); - break; - case 'git': - router.push(`${basePath}/int`); - break; - case 'env-vars': - router.push(`${basePath}/set/env`); - break; + // Load project from GraphQL + const loadProject = async (projectId: string) => { + try { + setLoading(true); + setError(null); + + const response = await client.getProject(projectId); + setProject(response.project); + + // Set deployments for the deployment tab + if (response.project?.deployments) { + setDeployments(response.project.deployments); + setFilteredDeployments(response.project.deployments); + } + } catch (err) { + console.error('Failed to load project:', err); + setError(err instanceof Error ? err.message : 'Failed to load project'); + } finally { + setLoading(false); } }; + // Refresh project data + const handleRefresh = () => { + if (id) { + loadProject(id); + } + }; + + const currentDeployment = project?.deployments?.find((d: any) => d.isCurrent); + const latestDeployment = project?.deployments?.[0]; // Assuming deployments are sorted by date + + // Handle tab changes WITHOUT navigation - just update local state + const handleTabChange = (value: string) => { + setActiveTab(value); + }; + + // Helper function to safely parse dates + const parseDate = (dateString: string | undefined) => { + if (!dateString) return null; + const date = new Date(dateString); + return isNaN(date.getTime()) ? null : date.getTime(); + }; + + // Generate activities from deployments + const generateActivities = () => { + if (!project?.deployments) return []; + + return project.deployments + .slice(0, 4) // Show last 4 deployments + .map((deployment: any) => ({ + username: deployment.createdBy?.name || 'Unknown', + branch: deployment.branch, + action: `deployed ${deployment.environment || 'production'}`, + time: parseDate(deployment.createdAt) ? relativeTimeMs(parseDate(deployment.createdAt)!) : 'Unknown time', + status: deployment.status + })); + }; + + const activities = generateActivities(); + + // Status badge component + const StatusBadge = ({ status }: { status: string }) => { + const getStatusColor = (status: string) => { + switch (status?.toUpperCase()) { + case 'COMPLETED': + case 'READY': + return 'bg-green-700/20 text-green-400'; + case 'BUILDING': + case 'DEPLOYING': + return 'bg-blue-700/20 text-blue-400'; + case 'ERROR': + case 'FAILED': + return 'bg-red-700/20 text-red-400'; + default: + return 'bg-gray-700/20 text-gray-400'; + } + }; + + return ( +
+ {status?.toUpperCase() || 'UNKNOWN'} +
+ ); + }; + + // Handle deployment logs + const handleViewLogs = () => { + const mockLogs = `[2025-02-12 10:03:12] INFO Starting deployment process for service: ${project?.name} +[2025-02-12 10:03:14] INFO Fetching latest commit from main branch (commit: a1b2c3d) +[2025-02-12 10:03:15] INFO Building Docker image: registry.company.com/${project?.name}:latest +[2025-02-12 10:03:26] INFO Running security scan on built image +[2025-02-12 10:03:30] INFO Pushing image to container registry +[2025-02-12 10:03:35] INFO Updating deployment configuration +[2025-02-12 10:03:40] INFO Scaling down old pods +[2025-02-12 10:03:42] INFO Scaling up new pods +[2025-02-12 10:03:50] INFO Running health checks on new pods +[2025-02-12 10:03:55] INFO Deployment completed successfully +[2025-02-12 10:03:56] INFO Service is now live at ${currentDeployment?.applicationDeploymentRecordData?.url}`; + + setDeploymentLogs(mockLogs); + setIsLogsOpen(true); + }; + + // Handle deployment deletion + const handleDeleteDeployment = async (deploymentId: string, deploymentBranch: string) => { + if (!confirm(`Are you sure you want to delete the deployment for branch "${deploymentBranch}"? This action cannot be undone.`)) { + return; + } + + try { + setLoading(true); + await client.deleteDeployment(deploymentId); + + // Refresh the project data to update the deployments list + await loadProject(id); + + // Show success message (you could replace with a toast notification) + alert('Deployment deleted successfully'); + } catch (error) { + console.error('Failed to delete deployment:', error); + alert('Failed to delete deployment. Please try again.'); + } finally { + setLoading(false); + } + }; + + // Handle deployment rollback + const handleRollbackDeployment = async (deploymentId: string, deploymentBranch: string) => { + if (!project?.id) return; + + if (!confirm(`Are you sure you want to rollback to the deployment for branch "${deploymentBranch}"?`)) { + return; + } + + try { + setLoading(true); + await client.rollbackDeployment(project.id, deploymentId); + + // Refresh the project data to update the deployments list + await loadProject(id); + + // Show success message + alert('Deployment rollback completed successfully'); + } catch (error) { + console.error('Failed to rollback deployment:', error); + alert('Failed to rollback deployment. Please try again.'); + } finally { + setLoading(false); + } + }; + + // Copy to clipboard function for settings + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + // You could add a toast notification here + }; + + // Settings page state + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const showMessage = (message: string, isError = false) => { + if (isError) { + setErrorMessage(message); + setSuccessMessage(""); + } else { + setSuccessMessage(message); + setErrorMessage(""); + } + setTimeout(() => { + setSuccessMessage(""); + setErrorMessage(""); + }, 3000); + }; + + // Handle project deletion + const handleDeleteProject = async () => { + if (!project) return; + + try { + setIsDeleting(true); + + await client.deleteProject(project.id); + + showMessage("Project deleted successfully"); + setIsDeleteModalOpen(false); + + // Navigate back to projects list after successful deletion + setTimeout(() => { + router.push('/projects'); + }, 1500); + + } catch (error) { + console.error("Failed to delete project:", error); + showMessage("Failed to delete project", true); + } finally { + setIsDeleting(false); + setIsDeleteModalOpen(false); + } + }; + + // Loading state + if (loading) { + return ( + +
+
Loading project data...
+
+
+ ); + } + + // Error state + if (error || !project) { + return ( + +
+ +
Project not found
+
+ {error ? `Error: ${error}` : 'The requested project could not be loaded.'} +
+
+ + +
+
+
+ ); + } + return ( -
{/* Take full width in bento grid */} - {/* Tabs navigation */} - +
+ {/* Tabs navigation - controlled locally */} + Overview Deployment @@ -137,13 +343,23 @@ export default function ProjectOverviewPage() {
- {getInitials(projectName || '')} + {getInitials(project.name || '')} -
-

{projectName}

+
+
+

{project.name}

+ {currentDeployment && ( + + )} +

- {deploymentUrl.replace(/^https?:\/\//, '')} + {currentDeployment?.applicationDeploymentRecordData?.url?.replace(/^https?:\/\//, '') || + latestDeployment?.applicationDeploymentRecordData?.url?.replace(/^https?:\/\//, '') || + 'No deployment URL'}

+ {project.description && ( +

{project.description}

+ )}
@@ -151,78 +367,120 @@ export default function ProjectOverviewPage() {
- Source + Production Branch
- {branch} + {project.prodBranch || 'main'}
- Deployment URL + Repository
- - {deploymentUrl} - + {project.repository ? ( + + {project.repository.replace('https://github.com/', '')} + + + ) : ( + No repository linked + )}
-
-
- - Deployment date +
+
+
+ + Last Deployment +
+
+ + {latestDeployment ? + (parseDate(latestDeployment.createdAt) ? + relativeTimeMs(parseDate(latestDeployment.createdAt)!) : + 'Invalid date') : + 'No deployments' + } + + {latestDeployment?.createdBy && ( + <> + by + + {getInitials(latestDeployment.createdBy.name || '')} + + {latestDeployment.createdBy.name} + + )} +
-
- - {relativeTimeMs(deploymentDate)} - - by - - {getInitials(deployedBy)} - - {deployedBy} + +
+
+ + Framework +
+
+ {project.framework || 'Unknown'} +
{/* Divider between project info and auction details */}
- {/* Auction Details section */} + {/* Deployment Details section */}
-

Auction Details

+

Deployment Details

-

Auction ID

-

{auctionId}

+

Project ID

+

{project.id}

-

Auction Status

-
- COMPLETED -
+

Organization

+

{project.organization?.name || 'Unknown'}

-
-
-

Deployer LRNs

-

{auctionId}

-
-
-

Deployer Funds Status

-
- RELEASED + {project.auctionId && ( +
+
+

Auction ID

+

{project.auctionId}

+
+
+

Funds Status

+
-
+ )} + + {project.deployers && project.deployers.length > 0 && ( +
+

Deployers ({project.deployers.length})

+
+ {project.deployers.slice(0, 2).map((deployer: any, index: number) => ( +
+ {deployer.deployerLrn} +
+ ))} + {project.deployers.length > 2 && ( +
+ And {project.deployers.length - 2} more... +
+ )} +
+
+ )}
@@ -231,7 +489,7 @@ export default function ProjectOverviewPage() {
- {/* Activity section - not in a card */} + {/* Activity section */}

@@ -239,29 +497,312 @@ export default function ProjectOverviewPage() {

- {activities.map((activity, index) => ( -
-
β€’
-
- {activity.username} - - {activity.branch} - {activity.action} + {activities.length > 0 ? ( + activities.map((activity, index) => ( +
+
β€’
+
+ {activity.username} + + {activity.branch} + {activity.action} +
+
{activity.time}
-
{activity.time}
+ )) + ) : ( +
+ No recent activity
- ))} + )}
- {/* These content sections won't be shown - we'll navigate to respective pages instead */} - - - - + +
+ {/* Filter Controls */} +
+
+
+ +
+ +
+ + + + +
+ + {/* Deployments List */} + {filteredDeployments.length > 0 ? ( + filteredDeployments.map((deployment) => ( +
+
+
+ + {deployment.branch} + + {deployment.isCurrent && ( + Current + )} +
+
+ + {!deployment.isCurrent && ( + + )} + +
+
+ +
+
+ URL: + + {deployment.applicationDeploymentRecordData?.url} + +
+
+ Created: + {parseDate(deployment.createdAt) ? + relativeTimeMs(parseDate(deployment.createdAt)!) : + 'Unknown date' + } +
+
+ Commit: + {deployment.commitHash?.substring(0, 8) || 'Unknown'} +
+
+ Created by: + {deployment.createdBy?.name || 'Unknown'} +
+
+
+ )) + ) : ( +
+ +

You have no deployments

+

+ Deploy your first version to see deployment history here. +

+
+ )} +
+
+ + + {/* Updated theme-aware settings content */} +
+ {/* Success/Error Messages */} + {successMessage && ( +
+ {successMessage} +
+ )} + {errorMessage && ( +
+ {errorMessage} +
+ )} + +
+

Project Info

+ +
+
+ + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ +
+

Delete Project

+ +

+ The project will be permanently deleted, including its deployments and domains. This action is + irreversible and cannot be undone. +

+ + +
+
+
+ + + + + + + +
+ + + {/* Delete Confirmation Modal */} + !isDeleting && setIsDeleteModalOpen(open)}> + + + Are you absolutely sure? + +
+

+ This action cannot be undone. This will permanently delete the project{" "} + "{project?.name}" and all associated deployments and domains. +

+
+ + + + +
+
); } \ No newline at end of file diff --git a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx index 35ce1da..1683b54 100644 --- a/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx +++ b/apps/deploy-fe/src/app/(web3-authenticated)/(dashboard)/projects/page.tsx @@ -1,72 +1,85 @@ 'use client' import { PageWrapper } from '@/components/foundation' import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe' -import type { Project } from '@octokit/webhooks-types' import { FixedProjectCard } from '@/components/projects/project/ProjectCard/FixedProjectCard' import { Button } from '@workspace/ui/components/button' import { useEffect, useState } from 'react' import { Shapes } from 'lucide-react' -import { useAuth, useUser } from '@clerk/nextjs' -import { useRepoData } from '@/hooks/useRepoData' +import { useGQLClient } from '@/context' +import type { Project } from '@workspace/gql-client' interface ProjectData { id: string name: string icon?: string deployments: any[] - // Additional fields from GitHub repo - full_name?: string - html_url?: string - updated_at?: string - default_branch?: string + repository?: string + framework?: string + description?: string } export default function ProjectsPage() { const [, setIsBalanceSufficient] = useState() - const [projects, setProjects] = useState([]) + const [projects, setProjects] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) - const { isLoaded: isAuthLoaded, userId } = useAuth() - const { isLoaded: isUserLoaded, user } = useUser() + const client = useGQLClient() - // Use the hook to fetch all repos (with an empty ID to get all) - const { repoData: allRepos, isLoading: reposLoading, error: reposError } = useRepoData(''); - - const handleConnectGitHub = () => { - window.open('https://accounts.clerk.dev/user', '_blank'); + const handleCreateProject = () => { + window.location.href = '/projects/github/ps/cr' } useEffect(() => { - // Process repos data when it's loaded - if (!reposLoading && allRepos) { - // Transform GitHub repos to match ProjectData interface - const projectData: ProjectData[] = allRepos.map((repo: any) => ({ - id: repo.id.toString(), - name: repo.name, - full_name: repo.full_name, - // Create a deployment object that matches your existing structure - deployments: [ - { - applicationDeploymentRecordData: { - url: repo.html_url - }, - branch: repo.default_branch, - createdAt: repo.updated_at, - createdBy: { - name: repo.owner?.login || 'Unknown' - } - } - ] - })); + loadAllProjects() + }, []) + + const loadAllProjects = async () => { + try { + setIsLoading(true) + setError(null) - setProjects(projectData); - setIsLoading(false); - } else if (!reposLoading && reposError) { - setError(reposError); - setIsLoading(false); + // First get organizations + const orgsResponse = await client.getOrganizations() + + if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) { + setProjects([]) + setIsLoading(false) + return + } + + // Get projects from all organizations + const allProjects: ProjectData[] = [] + + for (const org of orgsResponse.organizations) { + try { + const projectsResponse = await client.getProjectsInOrganization(org.slug) + + // Transform GraphQL projects to match ProjectData interface + const orgProjects: ProjectData[] = projectsResponse.projectsInOrganization.map((project: Project) => ({ + id: project.id, + name: project.name, + repository: project.repository, + framework: project.framework, + description: project.description, + deployments: project.deployments || [] + })) + + allProjects.push(...orgProjects) + } catch (orgError) { + console.error(`Failed to load projects for org ${org.slug}:`, orgError) + // Continue with other orgs even if one fails + } + } + + setProjects(allProjects) + } catch (err) { + console.error('Failed to load projects:', err) + setError(err instanceof Error ? err.message : 'Failed to load projects') + } finally { + setIsLoading(false) } - }, [allRepos, reposLoading, reposError]); + } return (

Error: {error}

- Please connect your GitHub account to see your repositories. + Failed to load your deployed projects. Please try again.

) : projects.length === 0 ? ( @@ -114,29 +124,77 @@ export default function ProjectsPage() {

Deploy your first app

- Once connected, you can import a repository from your account or start with one of our templates. + You don't have any deployed projects yet. Create your first project to get started.

) : ( // Custom grid that spans the entire bento layout
- {projects.map((project) => ( - - ))} + {projects.map((project) => { + // Get the current deployment for status + const currentDeployment = project.deployments.find(d => d.isCurrent) + const latestDeployment = project.deployments[0] // Assuming sorted by date + + // Determine status based on deployment + let status = 'pending' + if (currentDeployment || latestDeployment) { + const deployment = currentDeployment || latestDeployment + switch (deployment.status?.toUpperCase()) { + case 'READY': + case 'COMPLETED': + status = 'success' + break + case 'BUILDING': + case 'DEPLOYING': + status = 'in-progress' + break + case 'ERROR': + case 'FAILED': + status = 'failure' + break + default: + status = 'pending' + } + } + + // Format the project data to match what FixedProjectCard expects + const formattedProject = { + id: project.id, + name: project.name, + full_name: project.repository ? project.repository.replace('https://github.com/', '') : project.name, + repository: project.repository, + framework: project.framework, + description: project.description, + // Ensure deployments array is properly formatted + deployments: project.deployments.map(deployment => ({ + ...deployment, + // Make sure the date is in a format the card can parse + createdAt: deployment.createdAt, + applicationDeploymentRecordData: { + url: deployment.applicationDeploymentRecordData?.url || `https://${project.name.toLowerCase()}.example.com` + } + })) + } + + return ( + + ) + })}
)} diff --git a/apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx b/apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx new file mode 100644 index 0000000..33f8609 --- /dev/null +++ b/apps/deploy-fe/src/app/auth/github/backend-callback/page.tsx @@ -0,0 +1,166 @@ +// src/app/auth/github/backend-callback/page.tsx +'use client' +import { useEffect, useState } from 'react' +import { useSearchParams } from 'next/navigation' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/card' +import { CheckCircle2, AlertCircle, Loader2 } from 'lucide-react' +import { useGQLClient } from '@/context' + +export default function GitHubBackendCallbackPage() { + const searchParams = useSearchParams() + const [status, setStatus] = useState<'processing' | 'success' | 'error'>('processing') + const [message, setMessage] = useState('Processing GitHub authentication...') + const gqlClient = useGQLClient() + + useEffect(() => { + const handleCallback = async () => { + try { + // Get parameters from URL + const code = searchParams.get('code') + const state = searchParams.get('state') + const error = searchParams.get('error') + const errorDescription = searchParams.get('error_description') + + // Check for OAuth errors + if (error) { + throw new Error(errorDescription || `GitHub OAuth error: ${error}`) + } + + // Validate required parameters + if (!code) { + throw new Error('No authorization code received from GitHub') + } + + // Verify state parameter for security + const storedState = sessionStorage.getItem('github_oauth_state') + if (state !== storedState) { + throw new Error('Invalid state parameter - possible CSRF attack') + } + + // Clean up stored state + sessionStorage.removeItem('github_oauth_state') + + setMessage('Connecting to backend...') + + // Call backend's authenticateGitHub mutation + const result = await gqlClient.authenticateGitHub(code) + + if (result.authenticateGitHub?.token) { + setStatus('success') + setMessage('GitHub authentication successful!') + + // Notify parent window + if (window.opener) { + window.opener.postMessage({ + type: 'GITHUB_BACKEND_AUTH_SUCCESS', + token: result.authenticateGitHub.token + }, window.location.origin) + } + + // Close popup after a short delay + setTimeout(() => { + window.close() + }, 2000) + } else { + throw new Error('No token received from backend') + } + + } catch (error) { + console.error('GitHub OAuth callback error:', error) + setStatus('error') + setMessage(error instanceof Error ? error.message : 'Unknown error occurred') + + // Notify parent window of error + if (window.opener) { + window.opener.postMessage({ + type: 'GITHUB_BACKEND_AUTH_ERROR', + message: error instanceof Error ? error.message : 'Unknown error' + }, window.location.origin) + } + + // Close popup after delay even on error + setTimeout(() => { + window.close() + }, 5000) + } + } + + // Only run if we have search params (meaning this is the callback) + if (searchParams.toString()) { + handleCallback() + } + }, [searchParams, gqlClient]) + + const getStatusIcon = () => { + switch (status) { + case 'processing': + return + case 'success': + return + case 'error': + return + } + } + + const getStatusColor = () => { + switch (status) { + case 'processing': + return 'text-blue-800' + case 'success': + return 'text-green-800' + case 'error': + return 'text-red-800' + } + } + + return ( +
+ + +
+ {getStatusIcon()} +
+ GitHub Authentication + + {status === 'processing' && 'Processing your GitHub authentication...'} + {status === 'success' && 'Authentication completed successfully'} + {status === 'error' && 'Authentication failed'} + +
+ +
+

+ {message} +

+ + {status === 'success' && ( +
+ This window will close automatically... +
+ )} + + {status === 'error' && ( +
+
+ This window will close automatically in a few seconds. +
+ +
+ )} + + {status === 'processing' && ( +
+ Please wait while we complete the authentication process... +
+ )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/deploy-fe/src/app/layout.tsx b/apps/deploy-fe/src/app/layout.tsx index 9f557fd..5838072 100644 --- a/apps/deploy-fe/src/app/layout.tsx +++ b/apps/deploy-fe/src/app/layout.tsx @@ -4,6 +4,7 @@ import '@workspace/ui/globals.css' import type { Metadata } from 'next' import { Inter } from 'next/font/google' import { CheckBalanceWrapper } from '@/components/iframe/check-balance-iframe/CheckBalanceWrapper' +import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in' // Add root metadata with template pattern export const metadata: Metadata = { @@ -29,6 +30,9 @@ export default function RootLayout({
{children} +
+ +
diff --git a/apps/deploy-fe/src/app/test-connection/page.tsx b/apps/deploy-fe/src/app/test-connection/page.tsx new file mode 100644 index 0000000..f4ff77d --- /dev/null +++ b/apps/deploy-fe/src/app/test-connection/page.tsx @@ -0,0 +1,882 @@ +// src/app/test-connection/page.tsx +'use client' +import { useState, useEffect } from 'react' +import { PageWrapper } from '@/components/foundation' +import { DirectKeyAuth } from '@/components/DirectKeyAuth' +import { GQLTest } from '@/components/GQLTest' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@workspace/ui/components/tabs' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/card' +import { useGQLClient } from '@/context' +import { Button } from '@workspace/ui/components/button' +import { Input } from '@workspace/ui/components/input' +import { Label } from '@workspace/ui/components/label' +import { Loader2, AlertTriangle, CheckCircle2, GitBranch } from 'lucide-react' +import { toast } from 'sonner' +import { useRepoData } from '@/hooks/useRepoData' +import { useAuth, useUser, SignIn } from "@clerk/nextjs" +import { GitHubBackendAuth } from '@/components/GitHubBackendAuth' + +// Add this at the very top of your file +declare global { + interface Window { + Clerk?: { + session?: { + getToken: (options?: { template?: string }) => Promise + } + } + } +} + +export default function TestConnectionPage() { + // Get getToken from useAuth hook, not from user + const { isSignedIn, isLoaded: isClerkLoaded, getToken } = useAuth() + const { user, isLoaded: isUserLoaded } = useUser() + + // Authentication states + const [isWalletConnected, setIsWalletConnected] = useState(false) + const [isBackendConnected, setIsBackendConnected] = useState(false) + const [isGithubAuthed, setIsGithubAuthed] = useState(false) + + // Organization and deployment states + const [organizations, setOrganizations] = useState([]) + const [selectedOrg, setSelectedOrg] = useState('') + const [isDeploying, setIsDeploying] = useState(false) + const [deploymentResult, setDeploymentResult] = useState(null) + const [deploymentError, setDeploymentError] = useState(null) + + const [deployers, setDeployers] = useState([]) + const [selectedDeployer, setSelectedDeployer] = useState('') + const [deployersLoading, setDeployersLoading] = useState(false) + + // Form state + const [formData, setFormData] = useState({ + name: 'test-deployment', + repository: '', + branch: 'main', + }) + + // Contexts and hooks + const gqlClient = useGQLClient() + + // Use the useRepoData hook to get repositories (using Clerk's GitHub integration) + const { repoData: repositories } = useRepoData('') + + // Check if both authentications are complete + const isFullyAuthenticated = isWalletConnected && isBackendConnected && isSignedIn && isGithubAuthed + // Add this near your other useState declarations at the top of the component + const [manualToken, setManualToken] = useState('') + + // Update the function to use getToken from useAuth + const getClerkTokenForManualEntry = async () => { + if (!isSignedIn || !user) { + toast.error('Please sign in first') + return + } + + try { + console.log('Attempting to get token from useAuth...') + + // Method 1: Try getToken from useAuth hook + let token = null + + try { + token = await getToken() + console.log('Method 1 (getToken from useAuth) worked:', token ? 'SUCCESS' : 'NO TOKEN') + } catch (error) { + console.log('Method 1 failed:', error) + } + + // Method 2: Try with template parameter + if (!token) { + try { + token = await getToken({ template: 'github' }) + console.log('Method 2 (getToken with github template) worked:', token ? 'SUCCESS' : 'NO TOKEN') + } catch (error) { + console.log('Method 2 failed:', error) + } + } + + // Method 3: Try accessing window.Clerk (as mentioned in discussions) + if (!token && typeof window !== 'undefined' && window.Clerk) { + try { + token = await window.Clerk.session?.getToken() + console.log('Method 3 (window.Clerk.session.getToken) worked:', token ? 'SUCCESS' : 'NO TOKEN') + } catch (error) { + console.log('Method 3 failed:', error) + } + } + + // Method 4: Try window.Clerk with template + if (!token && typeof window !== 'undefined' && window.Clerk) { + try { + token = await window.Clerk.session?.getToken({ template: 'github' }) + console.log('Method 4 (window.Clerk with github template) worked:', token ? 'SUCCESS' : 'NO TOKEN') + } catch (error) { + console.log('Method 4 failed:', error) + } + } + + if (token) { + setManualToken(token) + // Copy to clipboard automatically + navigator.clipboard.writeText(token) + toast.success('Token extracted and copied to clipboard') + console.log('GitHub token from Clerk:', token.substring(0, 20) + '...') + } else { + toast.error('Unable to extract GitHub token. Check console for details.') + console.log('GitHub account object:', user.externalAccounts?.find(account => account.provider === 'github')) + } + } catch (error) { + console.error('Error getting token from Clerk:', error) + toast.error(`Failed to get token: ${error instanceof Error ? error.message : 'Unknown error'}`) + } + } + + // Check backend connection + const checkBackendConnection = async () => { + try { + // Test session + const response = await fetch('http://localhost:8000/auth/session', { + method: 'GET', + credentials: 'include', + }) + + if (response.ok) { + setIsBackendConnected(true) + console.log('Backend connected!') + + // Check if user has GitHub token in backend + await checkBackendGithubAuth() + + // Fetch organizations + try { + const orgsData = await gqlClient.getOrganizations() + console.log('Organizations:', orgsData) + setOrganizations(orgsData.organizations || []) + + // Set default org if available + if (orgsData.organizations && orgsData.organizations.length > 0) { + setSelectedOrg(orgsData.organizations[0].slug) + } + } catch (error) { + console.error('Error fetching organizations:', error) + } + } else { + setIsBackendConnected(false) + console.log('Backend not connected') + } + } catch (error) { + console.error('Error checking backend connection:', error) + setIsBackendConnected(false) + } + } + + // Check if user has GitHub token in the backend database + const checkBackendGithubAuth = async () => { + try { + // Try to get user data from backend + const userData = await gqlClient.getUser() + console.log('Backend user data:', userData) + + // Check if user has GitHub token in backend + setIsGithubAuthed(!!userData.user.gitHubToken) + } catch (error) { + console.error('Error checking backend GitHub auth:', error) + setIsGithubAuthed(false) + } + } + + // Sync GitHub token from Clerk to backend + // Check wallet connection status whenever the backend connection changes + useEffect(() => { + const checkWalletConnection = async () => { + if (isBackendConnected) { + try { + const response = await fetch('http://localhost:8000/auth/session', { + method: 'GET', + credentials: 'include', + }) + + setIsWalletConnected(response.ok) + } catch (error) { + console.error('Error checking wallet connection:', error) + setIsWalletConnected(false) + } + } + } + + checkWalletConnection() + }, [isBackendConnected]) + + // Check backend connection on mount + useEffect(() => { + if (isClerkLoaded && isUserLoaded) { + checkBackendConnection() + } + }, [isClerkLoaded, isUserLoaded]) + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData(prev => ({ ...prev, [name]: value })) + } + + // Add this function to fetch deployers: +const fetchDeployers = async () => { + try { + setDeployersLoading(true) + const deployersData = await gqlClient.getDeployers() + console.log('Available deployers:', deployersData) + setDeployers(deployersData.deployers || []) + + // Auto-select first deployer if available + if (deployersData.deployers && deployersData.deployers.length > 0) { + setSelectedDeployer(deployersData.deployers[0].deployerLrn) + } + } catch (error) { + console.error('Error fetching deployers:', error) + toast.error('Failed to fetch deployers') + } finally { + setDeployersLoading(false) + } +} + +// Add this useEffect to fetch deployers when backend is connected: +useEffect(() => { + if (isBackendConnected && isFullyAuthenticated) { + fetchDeployers() + } +}, [isBackendConnected, isFullyAuthenticated]) + +// Updated handleDeploy function: +const handleDeploy = async () => { + if (!isFullyAuthenticated) { + setDeploymentError('Complete authentication required. Please authenticate with both wallet and GitHub.') + return + } + + if (!selectedOrg) { + setDeploymentError('No organization selected') + return + } + + if (!formData.repository) { + setDeploymentError('No repository selected') + return + } + + if (!selectedDeployer) { + setDeploymentError('No deployer selected') + return + } + + setIsDeploying(true) + setDeploymentError(null) + setDeploymentResult(null) + + try { + console.log('πŸš€ Starting deployment with data:', { + ...formData, + organizationSlug: selectedOrg, + deployerLrn: selectedDeployer + }) + + // Validate repository format + if (!formData.repository.includes('/')) { + throw new Error('Repository must be in format "owner/repo-name"') + } + + const [owner, repo] = formData.repository.split('/') + if (!owner || !repo) { + throw new Error('Invalid repository format. Expected "owner/repo-name"') + } + + console.log('πŸ“€ Calling backend addProject mutation...') + + // Use the addProject mutation with deployer LRN + const result = await gqlClient.addProject( + selectedOrg, + { + name: formData.name, + repository: formData.repository, + prodBranch: formData.branch, + paymentAddress: "0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E", + txHash: "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + selectedDeployer, // Pass the deployer LRN here + undefined, // auctionParams + [] // environmentVariables + ) + + console.log('Project creation result:', result) + + if (result.addProject?.id) { + // Wait a moment to allow deployment to start + await new Promise(resolve => setTimeout(resolve, 2000)) + + // Get updated project data with deployments + const projectData = await gqlClient.getProject(result.addProject.id) + console.log('Project data with deployments:', projectData) + + setDeploymentResult({ + project: projectData.project, + message: 'Project created successfully!' + }) + + toast.success('Project deployed successfully!') + } else { + throw new Error('No project ID returned from creation') + } + } catch (error) { + console.error('Deployment failed:', error) + + let errorMessage = 'Unknown error' + if (error instanceof Error) { + errorMessage = error.message + } + + setDeploymentError(`Failed to deploy: ${errorMessage}`) + toast.error('Deployment failed') + } finally { + setIsDeploying(false) + } +} + + return ( + +
+ + + + Authentication Status + + + +
+
+
+ Wallet Connection: {isWalletConnected ? 'Connected' : 'Disconnected'} +
+ +
+
+ Backend Connection: {isBackendConnected ? 'Connected' : 'Disconnected'} +
+ +
+
+ Clerk Authentication: {isSignedIn ? 'Signed In' : 'Not Signed In'} +
+ +
+
+ GitHub (Backend): {isGithubAuthed ? 'Authenticated' : 'Not Authenticated'} +
+
+ +
+
+
+ {isFullyAuthenticated ? ( + + ) : ( + + )} + + {isFullyAuthenticated + ? 'All authentication requirements met - Ready to deploy!' + : 'Complete all authentication steps to enable deployment'} + +
+
+
+ + +
+
+ + + + Wallet Auth + Clerk Auth + GitHub Sync + GraphQL + Deployment + + + +

Wallet Authentication

+

+ This authenticates your wallet with the backend for payment processing and transaction signing. +

+ +
+ + +

Clerk Authentication

+

+ This provides GitHub authentication and user management through Clerk. +

+ + {!isSignedIn ? ( + + + Sign In with Clerk + + Sign in to access GitHub repositories and user management features + + + + + + + ) : ( + + + Clerk Authentication Status + + You are signed in with Clerk + + + +
+ + Successfully signed in with Clerk +
+ +
+

User: {user?.emailAddresses[0]?.emailAddress}

+

User ID: {user?.id}

+

GitHub Connected: { + user?.externalAccounts.find(account => account.provider === 'github') + ? 'Yes' : 'No' + }

+
+ + {!user?.externalAccounts.find(account => account.provider === 'github') && ( +
+

+ You need to connect your GitHub account in Clerk to proceed. +

+ +
+ )} +
+
+ )} +
+ + +

GitHub Authentication

+

+ This page manages two separate GitHub connections for different purposes. +

+ +
+ {/* Clerk GitHub Integration */} + + + Clerk GitHub Integration + + Provides repository access and user management through Clerk + + + +
+
+
+ Clerk Authentication: {isSignedIn ? 'Signed In' : 'Not Signed In'} +
+ +
+
account.provider === 'github') + ? 'bg-green-500' : 'bg-red-500' + }`}>
+ GitHub Connected to Clerk: { + isSignedIn && user?.externalAccounts.find(account => account.provider === 'github') + ? 'Yes' : 'No' + } +
+ + {repositories && repositories.length > 0 && ( +
+

Available Repositories (via Clerk)

+
+
    + {repositories.slice(0, 5).map((repo: any) => ( +
  • + {repo.full_name} +
  • + ))} + {repositories.length > 5 && ( +
  • + ... and {repositories.length - 5} more repositories +
  • + )} +
+
+
+ )} + + {/* Token extraction for debugging */} +
+

Debug: Token Extraction

+ + + {manualToken && ( +
+
+ {manualToken.substring(0, 40)}... +
+
+ Token extracted successfully (showing first 40 characters) +
+
+ )} +
+
+
+
+ + {/* Backend GitHub Authentication */} + setIsGithubAuthed(isAuth)} + /> + + {/* Status Summary */} + + + Authentication Summary + + Overview of all authentication systems + + + +
+
+ Clerk GitHub (Repository Access) +
+
account.provider === 'github') + ? 'bg-green-500' : 'bg-red-500' + }`}>
+ + {isSignedIn && user?.externalAccounts.find(account => account.provider === 'github') + ? 'Connected' : 'Not Connected'} + +
+
+ +
+ Backend GitHub (Deployments) +
+
+ {isGithubAuthed ? 'Connected' : 'Not Connected'} +
+
+ +
+ Wallet Authentication +
+
+ {isWalletConnected ? 'Connected' : 'Not Connected'} +
+
+
+ +
+
+ {isFullyAuthenticated ? ( + + ) : ( + + )} + + {isFullyAuthenticated + ? 'All systems connected - Ready for deployment!' + : 'Complete all authentication steps to enable deployment'} + +
+
+
+
+
+
+ + +

GraphQL Testing

+ +
+ + +

Deployment Testing

+ + {!isFullyAuthenticated ? ( + + + Complete Authentication Required + + You need to complete all authentication steps before deploying + + + +
+
+
+
+ Wallet Authentication: {isWalletConnected ? 'Complete' : 'Required'} +
+ +
+
+ Clerk Authentication: {isSignedIn ? 'Complete' : 'Required'} +
+ +
+
+ GitHub Backend Sync: {isGithubAuthed ? 'Complete' : 'Required'} +
+
+ +
+

Next Steps:

+
    + {!isWalletConnected &&
  1. Complete wallet authentication in the Wallet Auth tab
  2. } + {!isSignedIn &&
  3. Sign in with Clerk in the Clerk Auth tab
  4. } + {!isGithubAuthed &&
  5. Sync GitHub token in the GitHub Sync tab
  6. } +
+
+
+
+
+ ) : ( + + + Test Deployment + + Deploy a test project to verify deployment functionality + + + +
+ {organizations.length > 0 ? ( +
+ + +
+ ) : ( +
+ No organizations found. You need to be part of at least one organization. +
+ )} + + {/* Deployer Selection */} +
+ + {deployersLoading ? ( +
+
+
+ Loading deployers... +
+
+ ) : deployers.length > 0 ? ( + + ) : ( +
+

+ No deployers available. The backend needs to have deployers configured. +

+ +
+ )} +
+ +
+ + +
+ +
+ + {repositories && repositories.length > 0 ? ( + + ) : ( +
+

+ Enter the repository manually (format: owner/repo-name) +

+ +
+ )} +
+ +
+ + +
+ + + + {deploymentError && ( +
+ {deploymentError} +
+ )} + + {deploymentResult && ( +
+
+ +
+

{deploymentResult.message}

+

Project ID: {deploymentResult.project?.id}

+

Name: {deploymentResult.project?.name}

+

Repository: {deploymentResult.project?.repository}

+
+
+ +
+ + Show full project details + +
+                            {JSON.stringify(deploymentResult.project, null, 2)}
+                          
+
+
+ )} +
+
+
+ )} +
+
+
+ +
+

Hybrid Authentication Flow

+

+ This deployment system requires both wallet and GitHub authentication: +

+
+
+

Wallet Authentication (DirectKeyAuth)

+
    +
  • Provides Ethereum wallet connection
  • +
  • Enables transaction signing for payments
  • +
  • Required for deployment costs and blockchain operations
  • +
+
+
+

GitHub Authentication (Clerk)

+
    +
  • Provides access to GitHub repositories
  • +
  • Enables repository cloning and deployment
  • +
  • Required for backend deployment operations
  • +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/deploy-fe/src/components/AuthTest.tsx b/apps/deploy-fe/src/components/AuthTest.tsx new file mode 100644 index 0000000..ddedeea --- /dev/null +++ b/apps/deploy-fe/src/components/AuthTest.tsx @@ -0,0 +1,411 @@ +// src/components/SIWEAuth.tsx with raw signature approach +'use client' +import { useState, useEffect } from 'react' +import { useWallet } from '@/context/WalletContext' +import { Button } from '@workspace/ui/components/button' +import { CheckBalanceWrapper } from './iframe/check-balance-iframe/CheckBalanceWrapper' +import { CopyIcon } from 'lucide-react' + +// Generate a random nonce +function generateNonce() { + return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); +} + +export function SIWEAuth() { + const { wallet, isConnected, connect, disconnect } = useWallet() + const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'unauthenticated'>('checking') + const [sessionData, setSessionData] = useState(null) + const [isAuthenticating, setIsAuthenticating] = useState(false) + const [authError, setAuthError] = useState(null) + const [signedMessage, setSignedMessage] = useState(null) + const [messageToSign, setMessageToSign] = useState(null) + const [debugInfo, setDebugInfo] = useState('') + + // Check if we already have a session + const checkSession = async () => { + try { + setSessionStatus('checking') + const response = await fetch('http://localhost:8000/auth/session', { + method: 'GET', + credentials: 'include', + }) + + if (response.ok) { + const data = await response.json() + setSessionStatus('authenticated') + setSessionData(data) + console.log('Session check successful:', data) + } else { + setSessionStatus('unauthenticated') + setSessionData(null) + console.log('Session check failed:', await response.text()) + } + } catch (error) { + console.error('Error checking session:', error) + setSessionStatus('unauthenticated') + setSessionData(null) + } + } + + // Check session on component mount + useEffect(() => { + checkSession() + }, []) + + // Copy text to clipboard + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text) + .then(() => { + console.log('Text copied to clipboard') + }) + .catch(err => { + console.error('Could not copy text: ', err) + }) + } + + // Create a SIWE message with the correct Ethereum address + const createSiweMessage = () => { + // We want to try both our displayed address and the expected one from errors + // We'll use the displayed address by default + const ethAddress = '0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E'; + + const domain = window.location.host + const origin = window.location.origin + const chainId = 1 // Ethereum mainnet + const statement = 'Sign in With Ethereum.' + const nonce = generateNonce() + const issuedAt = new Date().toISOString() + + // IMPORTANT: This format must exactly match what the SiweMessage constructor expects + return `${domain} wants you to sign in with your Ethereum account: +${ethAddress} + +${statement} + +URI: ${origin} +Version: 1 +Chain ID: ${chainId} +Nonce: ${nonce} +Issued At: ${issuedAt}` + } + + // Generate the message for signing + const generateMessageToSign = async () => { + if (!wallet?.address) { + setAuthError('Wallet not connected') + return + } + + try { + setIsAuthenticating(true) + setAuthError(null) + + // Create a SIWE message with the Ethereum address + const message = createSiweMessage() + console.log('SIWE Message with Ethereum address:', message) + setDebugInfo(`Generated message with Ethereum address. IMPORTANT: Make sure "Ethereum" is selected in the wallet dropdown when signing.`) + + // Set the message to sign + setMessageToSign(message) + + } catch (error) { + console.error('Error generating message:', error) + setAuthError(`Error: ${error instanceof Error ? error.message : String(error)}`) + } finally { + setIsAuthenticating(false) + } + } + + // Check auth without sending signature + const checkAuthWithoutSignature = async () => { + try { + setIsAuthenticating(true) + setAuthError(null) + setDebugInfo('Trying auth without signature...') + + // Create API route to handle this + const response = await fetch('/api/dev-auth', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + address: '0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E' + }) + }) + + const data = await response.json() + console.log('Dev auth response:', data) + setDebugInfo(prev => `${prev}\nDev auth response: ${JSON.stringify(data)}`) + + if (response.ok && data.success) { + console.log('Dev auth successful!') + setDebugInfo(prev => `${prev}\nDev auth successful!`) + await checkSession() + } else { + throw new Error(`Dev auth failed: ${JSON.stringify(data)}`) + } + + } catch (error) { + console.error('Dev auth error:', error) + setAuthError(`Error: ${error instanceof Error ? error.message : String(error)}`) + } finally { + setIsAuthenticating(false) + } + } + + // Submit signature to validate + const submitSignature = async () => { + if (!messageToSign || !signedMessage) { + setAuthError('Missing message or signature') + return + } + + try { + setIsAuthenticating(true) + setAuthError(null) + setDebugInfo(prev => `${prev}\nSubmitting raw signature...`) + + // Log the original signature + console.log('Raw signature:', signedMessage) + setDebugInfo(prev => `${prev}\nRaw signature: ${signedMessage}`) + + // Try using the raw signature directly + const response = await fetch('http://localhost:8000/auth/validate', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + message: messageToSign, + signature: signedMessage + }) + }) + + let responseData = {} + try { + responseData = await response.json() + } catch (e) { + console.log('Error parsing response:', e) + } + + console.log('Validation response:', responseData) + setDebugInfo(prev => `${prev}\nValidation response: ${JSON.stringify(responseData)}`) + + // If successful, we're done + if (response.ok && responseData.success) { + console.log('Authentication successful!') + setDebugInfo(prev => `${prev}\nAuthentication successful!`) + + // Clear message and signature + setMessageToSign(null) + setSignedMessage(null) + + // Check if we now have a session + await checkSession() + return + } + + // If we get here, it failed + throw new Error(`Validation failed: ${JSON.stringify(responseData)}`) + + } catch (error) { + console.error('Authentication error:', error) + setAuthError(`Error: ${error instanceof Error ? error.message : String(error)}`) + setSessionStatus('unauthenticated') + } finally { + setIsAuthenticating(false) + } + } + + return ( +
+ {/* Hidden iframe for wallet connection */} + + +

Sign-In With Ethereum

+ +
+

Wallet Status

+

+ Status: + {isConnected ? 'Connected' : 'Disconnected'} + +

+ {wallet && wallet.address && ( +
+

Laconic Address: {wallet.address}

+

Ethereum Address: 0x1ac42F4A25Ae0137d10a825a2e33e32de0F6B57E

+
+ )} + +
+ {!isConnected ? ( + + ) : ( + + )} +
+
+ + {isConnected && sessionStatus !== 'authenticated' && ( +
+

Authentication

+ +
+

IMPORTANT:

+

When signing the message, make sure "Ethereum" is selected in the wallet's network dropdown.

+
+ + {!messageToSign ? ( +
+ + +
+

+ Alternative Authentication Methods +

+ +

+ This will try to create a session using a development-only endpoint. +

+
+
+ ) : ( +
+
+
+

Message to Sign:

+ +
+
{messageToSign}
+
+ +
+

+ 1. Copy the message above +

+

+ 2. Go to your wallet's "Sign Message" page + ( + Open Wallet Sign Page + ) +

+

+ 3. Make sure "Ethereum" is selected in the network dropdown +

+

+ 4. Paste the message and sign it +

+

+ 5. Copy the ENTIRE signature and paste it below +

+
+ +
+

Paste Signature:

+