Author SHA1 Message Date
NasSharaf c8c403d5c0 Fixed create project button 2025-09-30 18:19:56 -04:00
NasSharaf 4135acc6d8 Added payment code including hook, services, and configure-step modifications 2025-09-30 17:33:45 -04:00
NasSharaf 6393e9fa75 removed useOnboarding hook and used store.ts instead, modified all dependencies accordingly 2025-09-22 23:32:25 -04:00
NasSharaf d6853b48e7 Tried to save selectedOrg to go around no org selected error message 2025-09-11 12:37:58 -04:00
NasSharaf 2b7db89997 Merge pull request 'Update frontend app env for staging deployment' (#6) from deep-stack/laconic-deployer-frontend:ng-remove-fallback-token into nas/fix-filter-and-components
Reviewed-on: NasSharaf/laconic-deployer-frontend#6
2025-09-11 15:04:43 +00:00
nabarun 895c04de03 Add GITHUB_BACKEND_CLIENT_ID env in readme steps 2025-09-11 09:56:35 +05:30
nabarun f0a7cb97b4 Remove env GITHUB_FALLBACK_TOKEN 2025-09-11 09:55:44 +05:30
NasSharaf 685f8a8793 Merge pull request 'Add steps in readme to setup and run app locally' (#5) from deep-stack/laconic-deployer-frontend:ng-local-setup into nas/fix-filter-and-components
Reviewed-on: NasSharaf/laconic-deployer-frontend#5
2025-09-11 01:14:10 +00:00
nabarun 1445cb865b Add steps in readme to setup and run app locally 2025-09-09 13:53:36 +05:30
NasSharaf cd3cd7667a Fixed filtering and simplified octokit context 2025-09-04 12:05:20 -04:00
NasSharaf 3a2a6dca7e Removed flickering and updated form entry 2025-09-02 00:35:20 -04:00
NasSharaf ce851ef883 Add GitHub user filtering and core authentication improvements 2025-08-28 20:36:31 -04:00
31 changed files with 2195 additions and 920 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ CLERK_SECRET_KEY=
NEXT_PUBLIC_WALLET_IFRAME_URL= # wherever your wallet is running
NEXT_PUBLIC_LACONICD_CHAIN_ID= # the appropriate chain ID for your network
NEXT_PUBLIC_API_URL=
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID= # Client ID of your GitHub OAuth App
+27
View File
@@ -34,3 +34,30 @@ export async function getGitHubOrgs() {
avatarUrl: org.avatar_url
}))
}
export async function getGitHubToken() {
const { userId } = await auth()
if (!userId) {
throw new Error('Unauthorized')
}
const user = await currentUser()
const githubAccount = user?.externalAccounts.find(
(account) => account.provider === 'github'
)
if (!githubAccount) {
throw new Error('GitHub not connected')
}
// For server actions, we can access the external account token directly
// This is a simplified approach that uses the account's external ID as token
const token = githubAccount.externalId
if (!token) {
throw new Error('Failed to get GitHub token')
}
return token
}
@@ -5,7 +5,7 @@ import { ConfigureStep } from '@/components/onboarding/configure-step/configure-
import { ConnectStep } from '@/components/onboarding/connect-step/connect-step'
import { DeployStep } from '@/components/onboarding/deploy-step/deploy-step'
import { SuccessStep } from '@/components/onboarding/success-step/success-step'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { X } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/navigation'
@@ -20,7 +20,7 @@ export default function CreateProjectFlow() {
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const { currentStep, setCurrentStep, resetOnboarding } = useOnboarding()
const { currentStep, setCurrentStep } = useOnboarding()
// Handle hydration mismatch by waiting for mount
useEffect(() => {
@@ -32,7 +32,7 @@ export default function CreateProjectFlow() {
return () => {
// Optional cleanup actions
}
}, [resetOnboarding])
}, [])
// Handle closing the modal
const handleClose = () => {
@@ -2,11 +2,12 @@
import { PageWrapper } from '@/components/foundation'
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
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 { useGQLClient } from '@/context'
import { useUser } from '@clerk/nextjs'
import type { Project } from '@workspace/gql-client'
import { Button } from '@workspace/ui/components/button'
import { Shapes } from 'lucide-react'
import { useEffect, useState } from 'react'
interface ProjectData {
id: string
@@ -23,55 +24,104 @@ export default function ProjectsPage() {
const [projects, setProjects] = useState<ProjectData[]>([])
const [isLoading, setIsLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const client = useGQLClient()
const { user } = useUser()
const handleCreateProject = () => {
window.location.href = '/projects/github/ps/cr'
}
useEffect(() => {
loadAllProjects()
}, [])
if (user !== undefined) {
loadAllProjects()
}
}, [user])
const loadAllProjects = async () => {
try {
setIsLoading(true)
setError(null)
// First get organizations
const orgsResponse = await client.getOrganizations()
if (!orgsResponse.organizations || orgsResponse.organizations.length === 0) {
if (user === null) {
// User is not authenticated
setProjects([])
setIsLoading(false)
return
}
// Get the authenticated user's GitHub username
const githubAccount = user?.externalAccounts?.find(
(account) => account.provider === 'github'
)
const githubUsername = githubAccount?.username
if (!githubUsername) {
console.warn('No GitHub username found for user')
setProjects([])
setIsLoading(false)
return
}
console.log('🔍 Filtering projects for GitHub user:', githubUsername)
// 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)
const projectsResponse = await client.getProjectsInOrganization(
org.slug
)
// Filter projects by GitHub username and transform to ProjectData interface
const userProjects: ProjectData[] =
projectsResponse.projectsInOrganization
.filter((project: Project) => {
if (project.repository) {
const repoOwner = project.repository.split('/')[0]
console.log(
`🔍 Project ${project.name}: repo owner = ${repoOwner}, current user = ${githubUsername}`
)
return repoOwner.toLowerCase() === githubUsername.toLowerCase()
}
return true // Include projects without repository info
})
.map((project: Project) => ({
id: project.id,
name: project.name,
repository: project.repository,
framework: project.framework,
description: project.description,
deployments: project.deployments || []
}))
console.log(
`🔍 Found ${userProjects.length} projects for ${githubUsername} in org ${org.slug}`
)
allProjects.push(...userProjects)
} catch (orgError) {
console.error(`Failed to load projects for org ${org.slug}:`, orgError)
console.error(
`Failed to load projects for org ${org.slug}:`,
orgError
)
// Continue with other orgs even if one fails
}
}
console.log('🔍 Total filtered projects:', allProjects)
setProjects(allProjects)
} catch (err) {
console.error('Failed to load projects:', err)
@@ -80,7 +130,7 @@ export default function ProjectsPage() {
setIsLoading(false)
}
}
return (
<PageWrapper
header={{
@@ -107,7 +157,7 @@ export default function ProjectsPage() {
<p className="text-gray-400 text-center max-w-md mb-6">
Failed to load your deployed projects. Please try again.
</p>
<Button
<Button
className="bg-white text-black hover:bg-gray-200 flex items-center"
onClick={loadAllProjects}
>
@@ -124,13 +174,20 @@ export default function ProjectsPage() {
</div>
<h2 className="text-xl font-semibold mb-2">Deploy your first app</h2>
<p className="text-gray-400 text-center max-w-md mb-6">
You don't have any deployed projects yet. Create your first project to get started.
You don't have any deployed projects yet. Create your first project
to get started.
</p>
<Button
<Button
className="bg-white text-black hover:bg-gray-200 flex items-center"
onClick={handleCreateProject}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
className="mr-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
@@ -143,9 +200,11 @@ export default function ProjectsPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((project) => {
// Get the current deployment for status
const currentDeployment = project.deployments.find(d => d.isCurrent)
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) {
@@ -167,30 +226,34 @@ export default function ProjectsPage() {
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,
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 => ({
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`
url:
deployment.applicationDeploymentRecordData?.url ||
`https://${project.name.toLowerCase()}.example.com`
}
}))
}
return (
<FixedProjectCard
project={formattedProject}
key={project.id}
<FixedProjectCard
project={formattedProject}
key={project.id}
status={status as any}
/>
)
@@ -198,7 +261,7 @@ export default function ProjectsPage() {
</div>
</div>
)}
{/* Wrap in try/catch to prevent breaking if there are issues */}
{(() => {
try {
@@ -208,12 +271,12 @@ export default function ProjectsPage() {
isPollingEnabled={false}
amount="1"
/>
);
)
} catch (error) {
console.error('Failed to render CheckBalanceIframe:', error);
return null;
console.error('Failed to render CheckBalanceIframe:', error)
return null
}
})()}
</PageWrapper>
)
}
}
+26 -16
View File
@@ -1,17 +1,17 @@
'use client'
import { useEffect, useState } from 'react'
import { PageWrapper } from '@/components/foundation'
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
import { useAuth, useUser } from '@clerk/nextjs'
import { Button } from '@workspace/ui/components/button'
import { Shapes } from 'lucide-react'
import Link from 'next/link'
import { useAuth, useUser } from '@clerk/nextjs'
import { LoadingOverlay } from '@/components/foundation/loading/loading-overlay'
import { useEffect, useState } from 'react'
export default function HomePage() {
const [isLoading, setIsLoading] = useState<boolean>(true)
const { isLoaded: isAuthLoaded, userId } = useAuth()
const { isLoaded: isUserLoaded } = useUser()
useEffect(() => {
// Check authentication status
if (isAuthLoaded && isUserLoaded) {
@@ -32,7 +32,9 @@ export default function HomePage() {
<PageWrapper
header={{
title: 'Welcome to Laconic Deploy',
actions: userId ? [{ label: 'Create Project', href: '/projects/create' }] : []
actions: userId
? [{ label: 'Create Project', href: '/projects/github/ps/cr' }]
: []
}}
layout="bento"
className="pb-0"
@@ -44,11 +46,14 @@ export default function HomePage() {
<div className="mb-6">
<Shapes size={80} className="stroke-current" />
</div>
<h1 className="text-4xl font-bold mb-6">Welcome to Laconic Deploy</h1>
<h1 className="text-4xl font-bold mb-6">
Welcome to Laconic Deploy
</h1>
<p className="text-xl text-muted-foreground mb-8">
Deploy your applications quickly and securely on the decentralized web.
Deploy your applications quickly and securely on the decentralized
web.
</p>
<div className="flex gap-4">
<Button asChild size="lg">
<Link href="/projects">View My Projects</Link>
@@ -64,24 +69,29 @@ export default function HomePage() {
<div className="mb-6">
<Shapes size={80} className="stroke-current" />
</div>
<h1 className="text-4xl font-bold mb-6">Decentralized Web Deployment</h1>
<h1 className="text-4xl font-bold mb-6">
Decentralized Web Deployment
</h1>
<p className="text-xl text-muted-foreground mb-8">
Deploy your applications securely and efficiently with Laconic's web3 deployment platform.
Connect your wallet to get started.
Deploy your applications securely and efficiently with Laconic's
web3 deployment platform. Connect your wallet to get started.
</p>
<Button onClick={handleConnectWallet} size="lg" className="mb-4">
Connect Wallet
</Button>
<p className="text-sm text-muted-foreground mt-4">
Already have an account? <Link href="/sign-in" className="text-primary hover:underline">Sign in</Link>
Already have an account?{' '}
<Link href="/sign-in" className="text-primary hover:underline">
Sign in
</Link>
</p>
</div>
)}
</div>
{/* Removed the CheckBalanceIframe component that was causing errors */}
</PageWrapper>
)
}
}
@@ -0,0 +1,43 @@
// src/components/iframe/transaction-approval/TransactionApprovalModal.tsx
'use client'
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
interface TransactionApprovalModalProps {
isVisible: boolean
onClose?: () => void
}
const WALLET_IFRAME_URL = (process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000') + '/wallet-embed'
export function TransactionApprovalModal({
isVisible,
onClose
}: TransactionApprovalModalProps) {
if (!isVisible) return null
return (
<Dialog open={isVisible} onOpenChange={(open) => !open && onClose?.()}>
<DialogContent className="max-w-md">
<VisuallyHidden>
<DialogTitle>Approve Transaction</DialogTitle>
</VisuallyHidden>
<div className="w-full h-[500px]">
<iframe
id="transaction-approval-iframe"
src={WALLET_IFRAME_URL}
className="w-full h-full border-0"
title="Approve Transaction"
sandbox="allow-scripts allow-same-origin allow-forms"
/>
</div>
<div className="text-center text-sm text-muted-foreground mt-4">
Please approve the transaction in your wallet
</div>
</DialogContent>
</Dialog>
)
}
@@ -10,7 +10,7 @@
import { ConfigureStep } from '@/components/onboarding/configure-step'
import { ConnectStep } from '@/components/onboarding/connect-step'
import { DeployStep } from '@/components/onboarding/deploy-step'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { ScrollArea } from '@workspace/ui/components/scroll-area'
import { SidebarNav } from './sidebar'
@@ -241,16 +241,26 @@
// export default OnboardingDialog
'use client'
import { useState } from 'react'
import { X, Github } from 'lucide-react'
import { Button } from '@workspace/ui/components/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { ScrollableRepoList } from '@/components/onboarding/connect-step/repository-list'
import { TemplateList } from '@/components/onboarding/connect-step/template-list'
import { useOnboarding } from '@/components/onboarding/store'
import { useRepoData } from '@/hooks/useRepoData'
import { Dialog, DialogContent, DialogTitle } from '@workspace/ui/components/dialog'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import { Button } from '@workspace/ui/components/button'
import {
Dialog,
DialogContent,
DialogTitle
} from '@workspace/ui/components/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@workspace/ui/components/select'
import { Github, X } from 'lucide-react'
import { useState } from 'react'
interface OnboardingDialogProps {
trigger?: React.ReactNode
@@ -269,18 +279,21 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
defaultOpen = false,
onClose
}) => {
const { nextStep, setFormData, formData, currentStep, previousStep } = useOnboarding()
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
const { nextStep, setFormData, formData, currentStep, previousStep } =
useOnboarding()
const [selectedRepo, setSelectedRepo] = useState<string>(
formData.githubRepo || ''
)
const [isImportMode, setIsImportMode] = useState(true)
const [isOpen, setIsOpen] = useState(defaultOpen)
const { repoData: repositories, isLoading } = useRepoData('')
// Handle repository selection
const handleRepoSelect = (repo: string) => {
setSelectedRepo(repo)
setFormData({ githubRepo: repo })
}
// Handle mode toggle between import and template
const toggleMode = (mode: 'import' | 'template') => {
setIsImportMode(mode === 'import')
@@ -293,7 +306,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
onClose()
}
}
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{trigger && trigger}
@@ -301,25 +314,48 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<VisuallyHidden>
<DialogTitle>Onboarding</DialogTitle>
</VisuallyHidden>
<div className="flex h-full w-full">
{/* Left sidebar with steps */}
<div className="w-[280px] bg-zinc-950 p-8 relative overflow-hidden">
{/* Laconic logo */}
<div className="flex items-center mb-12">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="mr-2">
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="mr-2"
>
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white" />
</svg>
<span className="text-white text-xl font-bold">LACONIC</span>
</div>
{/* Steps */}
<div className="space-y-10">
{/* Connect step */}
<div className="flex">
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'connect' ? 'text-black' : 'text-zinc-400'}>
<div
className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
>
<svg
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={
currentStep === 'connect'
? 'text-black'
: 'text-zinc-400'
}
>
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
@@ -327,16 +363,40 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}>Connect</h3>
<p className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}>Connect and import a GitHub repo</p>
<h3
className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}
>
Connect
</h3>
<p
className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}
>
Connect and import a GitHub repo
</p>
</div>
</div>
{/* Configure step */}
<div className="flex">
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'configure' ? 'text-black' : 'text-zinc-400'}>
<div
className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
>
<svg
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={
currentStep === 'configure'
? 'text-black'
: 'text-zinc-400'
}
>
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
@@ -344,59 +404,110 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}>Configure</h3>
<p className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}>Define the deployment type</p>
<h3
className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}
>
Configure
</h3>
<p
className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}
>
Define the deployment type
</p>
</div>
</div>
{/* Deploy step */}
<div className="flex">
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className={currentStep === 'deploy' ? 'text-black' : 'text-zinc-400'}>
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<div
className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
>
<svg
viewBox="0 0 24 24"
width="20"
height="20"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className={
currentStep === 'deploy'
? 'text-black'
: 'text-zinc-400'
}
>
<rect
x="2"
y="2"
width="20"
height="20"
rx="5"
ry="5"
></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'deploy' ? 'text-white' : 'text-zinc-400'}`}>Deploy</h3>
<p className={`text-sm ${currentStep === 'deploy' ? 'text-zinc-400' : 'text-zinc-500'}`}>Review and confirm deployment</p>
<h3
className={`font-medium text-base ${currentStep === 'deploy' ? 'text-white' : 'text-zinc-400'}`}
>
Deploy
</h3>
<p
className={`text-sm ${currentStep === 'deploy' ? 'text-zinc-400' : 'text-zinc-500'}`}
>
Review and confirm deployment
</p>
</div>
</div>
</div>
{/* Background shape (decorative) */}
<div className="absolute -bottom-32 -left-32 w-64 h-64 rounded-full bg-zinc-900 opacity-50"></div>
</div>
{/* Main content */}
<div className="flex-1 bg-zinc-950 p-8 relative">
{/* Close button */}
<button
<button
className="absolute top-4 right-4 text-zinc-400 hover:text-white"
onClick={() => handleOpenChange(false)}
>
<X size={24} />
</button>
{currentStep === 'connect' && (
<div className="max-w-md mx-auto mt-8">
{/* Connect icon */}
<div className="flex justify-center mb-6">
<svg viewBox="0 0 24 24" width="48" height="48" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" className="text-white">
<svg
viewBox="0 0 24 24"
width="48"
height="48"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
className="text-white"
>
<path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3"></path>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
</div>
{/* Connect header */}
<h2 className="text-2xl font-medium text-white text-center mb-2">Connect</h2>
<h2 className="text-2xl font-medium text-white text-center mb-2">
Connect
</h2>
<p className="text-center text-zinc-400 mb-8">
Connect and import a GitHub repo or start from a template
</p>
{/* Git account selector */}
<div className="mb-4">
<Select defaultValue="git-account">
@@ -411,25 +522,25 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
</SelectContent>
</Select>
</div>
{/* Mode buttons */}
<div className="grid grid-cols-2 gap-2 mb-4">
<Button
variant={isImportMode ? "default" : "outline"}
variant={isImportMode ? 'default' : 'outline'}
className={`py-6 ${isImportMode ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800 text-zinc-400'}`}
onClick={() => toggleMode('import')}
>
Import a repository
</Button>
<Button
variant={!isImportMode ? "default" : "outline"}
variant={!isImportMode ? 'default' : 'outline'}
className={`py-6 ${!isImportMode ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800 text-zinc-400'}`}
onClick={() => toggleMode('template')}
>
Start with a template
</Button>
</div>
{/* Repository or template list */}
{isImportMode ? (
<div className="mb-8">
@@ -445,11 +556,11 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<TemplateList onSelect={() => {}} />
</div>
)}
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-8">
<Button
variant="outline"
<Button
variant="outline"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
disabled={true}
>
@@ -460,8 +571,8 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={nextStep}
disabled={!selectedRepo && isImportMode}
>
@@ -470,24 +581,26 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
</div>
</div>
)}
{currentStep === 'configure' && (
<div className="max-w-md mx-auto mt-8">
{/* Configure content will go here */}
<div className="text-center">
<h2 className="text-2xl font-medium text-white mb-2">Configure</h2>
<h2 className="text-2xl font-medium text-white mb-2">
Configure
</h2>
<p className="text-zinc-400 mb-8">
Define your deployment configuration
</p>
<div className="text-center mt-16 text-zinc-400">
Configure step content will be implemented here
</div>
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-16">
<Button
variant="outline"
<Button
variant="outline"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
onClick={() => previousStep()}
>
@@ -498,8 +611,8 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<div className="w-12 h-1 rounded-full bg-blue-600"></div>
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
onClick={nextStep}
>
Next
@@ -508,24 +621,26 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
</div>
</div>
)}
{currentStep === 'deploy' && (
<div className="max-w-md mx-auto mt-8">
{/* Deploy content will go here */}
<div className="text-center">
<h2 className="text-2xl font-medium text-white mb-2">Deploy</h2>
<h2 className="text-2xl font-medium text-white mb-2">
Deploy
</h2>
<p className="text-zinc-400 mb-8">
Review and confirm your deployment
</p>
<div className="text-center mt-16 text-zinc-400">
Deploy step content will be implemented here
</div>
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-16">
<Button
variant="outline"
<Button
variant="outline"
className="text-zinc-400 bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
onClick={() => previousStep()}
>
@@ -536,9 +651,7 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
<div className="w-12 h-1 rounded-full bg-zinc-800"></div>
<div className="w-12 h-1 rounded-full bg-blue-600"></div>
</div>
<Button
className="bg-blue-600 hover:bg-blue-700 text-white"
>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">
Deploy
</Button>
</div>
@@ -552,4 +665,4 @@ const OnboardingDialog: React.FC<OnboardingDialogProps> = ({
)
}
export default OnboardingDialog
export default OnboardingDialog
@@ -1,7 +1,7 @@
'use client'
import { GitBranch, Settings, Box } from 'lucide-react'
import type { Step } from '@/components/onboarding/useOnboarding'
import type { Step } from '@/components/onboarding/types'
import { Box, GitBranch, Settings } from 'lucide-react'
interface OnboardingSidebarProps {
currentStep: Step
@@ -12,58 +12,101 @@ export function OnboardingSidebar({ currentStep }: OnboardingSidebarProps) {
<div className="w-[280px] bg-zinc-950 p-8 relative overflow-hidden">
{/* Laconic logo */}
<div className="flex items-center mb-12">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="mr-2">
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white"/>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="mr-2"
>
<path d="M12 16L4 8L12 0L20 8L12 16Z" fill="white" />
</svg>
<span className="text-white text-xl font-bold">LACONIC</span>
</div>
{/* Steps */}
<div className="space-y-10">
{/* Connect step */}
<div className="flex">
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
<GitBranch className={`h-5 w-5 ${currentStep === 'connect' ? 'text-black' : 'text-zinc-400'}`} />
<div
className={`w-10 h-10 rounded-lg ${currentStep === 'connect' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
>
<GitBranch
className={`h-5 w-5 ${currentStep === 'connect' ? 'text-black' : 'text-zinc-400'}`}
/>
</div>
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}>Connect</h3>
<p className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}>Connect and import a GitHub repo</p>
<h3
className={`font-medium text-base ${currentStep === 'connect' ? 'text-white' : 'text-zinc-400'}`}
>
Connect
</h3>
<p
className={`text-sm ${currentStep === 'connect' ? 'text-zinc-400' : 'text-zinc-500'}`}
>
Connect and import a GitHub repo
</p>
</div>
</div>
{/* Configure step */}
<div className="flex">
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
<Settings className={`h-5 w-5 ${currentStep === 'configure' ? 'text-black' : 'text-zinc-400'}`} />
<div
className={`w-10 h-10 rounded-lg ${currentStep === 'configure' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
>
<Settings
className={`h-5 w-5 ${currentStep === 'configure' ? 'text-black' : 'text-zinc-400'}`}
/>
</div>
<div className="w-px h-16 bg-zinc-800 mx-auto mt-2"></div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}>Configure</h3>
<p className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}>Define the deployment type</p>
<h3
className={`font-medium text-base ${currentStep === 'configure' ? 'text-white' : 'text-zinc-400'}`}
>
Configure
</h3>
<p
className={`text-sm ${currentStep === 'configure' ? 'text-zinc-400' : 'text-zinc-500'}`}
>
Define the deployment type
</p>
</div>
</div>
{/* Deploy step */}
<div className="flex">
<div className="mr-4">
<div className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}>
<Box className={`h-5 w-5 ${currentStep === 'deploy' || currentStep === 'success' ? 'text-black' : 'text-zinc-400'}`} />
<div
className={`w-10 h-10 rounded-lg ${currentStep === 'deploy' || currentStep === 'success' ? 'bg-white' : 'bg-zinc-800'} flex items-center justify-center`}
>
<Box
className={`h-5 w-5 ${currentStep === 'deploy' || currentStep === 'success' ? 'text-black' : 'text-zinc-400'}`}
/>
</div>
</div>
<div>
<h3 className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success' ? 'text-white' : 'text-zinc-400'}`}>Deploy</h3>
<p className={`text-sm ${currentStep === 'deploy' || currentStep === 'success' ? 'text-zinc-400' : 'text-zinc-500'}`}>Review and confirm deployment</p>
<h3
className={`font-medium text-base ${currentStep === 'deploy' || currentStep === 'success' ? 'text-white' : 'text-zinc-400'}`}
>
Deploy
</h3>
<p
className={`text-sm ${currentStep === 'deploy' || currentStep === 'success' ? 'text-zinc-400' : 'text-zinc-500'}`}
>
Review and confirm deployment
</p>
</div>
</div>
</div>
{/* Background shape (decorative) */}
<div className="absolute -bottom-32 -left-32 w-64 h-64 rounded-full bg-zinc-900 opacity-50"></div>
</div>
)
}
}
@@ -1,7 +1,7 @@
// src/components/onboarding/configure-step/configure-step.tsx
'use client'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { useGQLClient } from '@/context'
import { useWallet } from '@/context/WalletContext'
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
@@ -28,6 +28,8 @@ import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { adaptDeployers } from '../../../utils/typeAdapters'
import { usePayment } from '@/hooks/usePayment'
import { TransactionApprovalModal } from '@/components/iframe/transaction-approval/TransactionApprovalModal'
interface Deployer {
deployerLrn: string
@@ -44,6 +46,7 @@ interface Organization {
export function ConfigureStep() {
const { nextStep, previousStep, setFormData, formData } = useOnboarding()
const { processPayment, calculateAmount, isLoading: isPaymentLoading, isModalVisible, setIsModalVisible } = usePayment()
const { resolvedTheme } = useTheme()
const [mounted, setMounted] = useState(false)
@@ -53,19 +56,13 @@ export function ConfigureStep() {
const [isLoadingDeployers, setIsLoadingDeployers] = useState(true)
const [isLoadingOrgs, setIsLoadingOrgs] = useState(true)
// Form state
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>(
(formData.deploymentType as 'auction' | 'lrn') || 'lrn' // Default to LRN for simplicity
)
const [numberOfDeployers, setNumberOfDeployers] = useState<string>(
formData.deployerCount || '1'
)
const [maxPrice, setMaxPrice] = useState<string>(formData.maxPrice || '1000')
const [selectedLrn, setSelectedLrn] = useState<string>(
formData.selectedLrn || ''
)
// Form state - using local state since these aren't in the simplified store
const [deployOption, setDeployOption] = useState<'auction' | 'lrn'>('lrn') // Default to LRN for simplicity
const [numberOfDeployers, setNumberOfDeployers] = useState<string>('1')
const [maxPrice, setMaxPrice] = useState<string>('1000')
const [selectedLrn, setSelectedLrn] = useState<string>('')
const [selectedOrg, setSelectedOrg] = useState<string>(
formData.selectedOrg || ''
formData.organizationSlug || ''
)
const [envVars, setEnvVars] = useState<
{ key: string; value: string; environments: string[] }[]
@@ -88,19 +85,8 @@ export function ConfigureStep() {
}
}, [mounted])
// Initialize environment variables from formData if available
useEffect(() => {
if (
formData.environmentVariables &&
Array.isArray(formData.environmentVariables)
) {
setEnvVars(
formData.environmentVariables.length > 0
? formData.environmentVariables
: [{ key: '', value: '', environments: ['Production'] }]
)
}
}, [formData.environmentVariables])
// Environment variables are managed locally
// (Removed environment variables initialization since not in simple store)
// Fetch deployers from backend
const fetchDeployers = async () => {
@@ -150,6 +136,17 @@ export function ConfigureStep() {
}
}
// Auto-save selectedOrg to form data whenever it changes (including auto-selection)
useEffect(() => {
if (selectedOrg) {
console.log('Auto-saving selectedOrg to form data:', selectedOrg)
setFormData({
organizationSlug: selectedOrg,
selectedOrg: selectedOrg
})
}
}, [selectedOrg])
// Add an empty environment variable row
const addEnvVar = () => {
setEnvVars([
@@ -217,8 +214,27 @@ export function ConfigureStep() {
return true
}
// Calculate payment amount for display
const paymentAmount = calculateAmount({
deploymentType: deployOption,
selectedDeployer,
maxPrice,
deployerCount: numberOfDeployers
})
// Show payment amount in UI
const showPaymentRequired = () => {
if (deployOption === 'lrn' && selectedDeployer?.minimumPayment) {
return true
}
if (deployOption === 'auction' && paymentAmount !== '0') {
return true
}
return false
}
// Handle next step
const handleNext = () => {
const handleNext = async () => {
if (!canProceed()) {
toast.error('Please complete all required fields')
return
@@ -229,20 +245,78 @@ export function ConfigureStep() {
(env) => env.key.trim() && env.value.trim()
)
let paymentData = {
txHash: '',
paymentAddress: wallet?.address || ''
}
// Process payment if required
if (showPaymentRequired()) {
const paymentResult = await processPayment({
deploymentType: deployOption,
selectedDeployer,
maxPrice,
deployerCount: numberOfDeployers
})
if (!paymentResult) {
// Payment failed - error already shown by usePayment hook
return
}
paymentData = {
txHash: paymentResult.txHash,
paymentAddress: wallet?.address || ''
}
}
// Save configuration to form data
setFormData({
deploymentType: deployOption,
deployerCount: numberOfDeployers,
maxPrice: maxPrice,
selectedLrn: selectedLrn,
organizationSlug: selectedOrg,
selectedOrg: selectedOrg,
paymentAddress: wallet?.address,
environmentVariables: validEnvVars
selectedLrn: selectedLrn,
deploymentType: deployOption,
maxPrice: maxPrice,
deployerCount: numberOfDeployers,
environmentVariables: validEnvVars,
...paymentData //Add payments
})
nextStep()
}
// Update your UI to show payment info
const renderPaymentInfo = () => {
if (!showPaymentRequired()) return null
return (
<div className="mb-6">
<Label className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}>
Payment Required
</Label>
<div className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}>
<div className="text-sm space-y-1">
<div className="flex justify-between">
<span className="text-muted-foreground">Amount:</span>
<span className="font-mono">{paymentAmount} alnt</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">From:</span>
<span className="font-mono text-xs">{wallet?.address}</span>
</div>
</div>
</div>
</div>
)
}
// Update your Next button
const getNextButtonText = () => {
if (isPaymentLoading) return 'Processing Payment...'
if (showPaymentRequired()) return 'Pay and Continue'
return 'Continue'
}
// Don't render UI until after mount to prevent hydration mismatch
if (!mounted) {
return null
@@ -251,11 +325,9 @@ export function ConfigureStep() {
// Determine if dark mode is active
const isDarkMode = resolvedTheme === 'dark'
// Get deployment mode info
const isTemplateMode = formData.deploymentMode === 'template'
const selectedItem = isTemplateMode
? formData.template?.name
: formData.githubRepo
// Get deployment mode info - determine from available data
const isTemplateMode = !!formData.framework && !formData.repoName
const selectedItem = isTemplateMode ? formData.framework : formData.repoName
return (
<div className="w-full h-full flex flex-col p-8 overflow-y-auto">
@@ -540,20 +612,8 @@ export function ConfigureStep() {
)}
{/* Payment Address */}
<div className="mb-6">
<Label
className={`text-sm mb-2 block ${isDarkMode ? 'text-zinc-400' : 'text-zinc-700'}`}
>
Payment Address
</Label>
<div
className={`p-3 border rounded-md bg-muted ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<div className="text-sm font-mono break-all">
{wallet?.address || 'No wallet connected'}
</div>
</div>
</div>
{/* Add payment info section */}
{renderPaymentInfo()}
{/* Environment Variables */}
<div className="mb-6">
@@ -628,23 +688,30 @@ export function ConfigureStep() {
{/* Navigation buttons */}
<div className="flex justify-between items-center mt-4">
<Button
variant="outline"
className={`bg-transparent ${isDarkMode ? 'text-zinc-400 border-zinc-700' : 'text-zinc-600 border-zinc-300'}`}
onClick={previousStep}
>
Previous
</Button>
<Button
variant="default"
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
onClick={handleNext}
disabled={!canProceed()}
>
Next
</Button>
</div>
<Button
variant="outline"
className={`bg-transparent ${isDarkMode ? 'text-zinc-400 border-zinc-700' : 'text-zinc-600 border-zinc-300'}`}
onClick={previousStep}
disabled={isPaymentLoading}
>
Previous
</Button>
<Button
variant="default"
className={`${isDarkMode ? 'bg-zinc-800' : 'bg-zinc-800'} text-white hover:bg-zinc-700`}
onClick={handleNext}
disabled={!canProceed() || isPaymentLoading}
>
{getNextButtonText()}
</Button>
</div>
</div>
{/* Transaction Approval Modal */}
<TransactionApprovalModal
isVisible={isModalVisible}
onClose={() => setIsModalVisible(false)}
/>
</div>
)
}
@@ -1,68 +0,0 @@
import { FileCog } from 'lucide-react'
import { useState } from 'react'
import { useOnboarding } from '../store'
/**
* Second step in the onboarding flow
* Handles deployment configuration and environment setup
*
* Features:
* - Deployment type selection (auction/LRN)
* - Environment variable configuration
* - Account selection
*
* @component
*/
export function ConfigureStep() {
const { formData, setFormData } = useOnboarding()
const [activeTab, setActiveTab] = useState<'create-auction' | 'deployer-lrn'>(
'create-auction'
)
const [environments, setEnvironments] = useState({
production: false,
preview: false,
development: false
})
// const handleEnvironmentChange = (env: keyof typeof environments) => {
// setEnvironments((prev) => ({
// ...prev,
// [env]: !prev[env],
// }))
// setFormData({
// environmentVars: {
// ...formData.environmentVars,
// [env]: !environments[env],
// },
// })
// }
return (
<div className="w-full">
<div className="max-w-2xl mx-auto space-y-8">
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section with icon and description */}
<div className="flex flex-col items-center gap-1">
<FileCog className="w-16 h-16 text-foreground" />
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">
Configure
</h2>
<p className="text-base text-muted-foreground text-center">
Set the deployer LRN for a single deployment or by creating a
deployer auction for multiple deployments
</p>
</div>
</div>
Content sections will be placed here: 1. Deployment type tabs
(auction/LRN) 2. Configuration forms 3. Environment variables 4.
Account selection ...content here/
{/* <Configure /> */}
</div>
</div>
</div>
</div>
)
}
@@ -2,7 +2,7 @@
'use client'
import { GitHubBackendAuth } from '@/components/GitHubBackendAuth'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { AVAILABLE_TEMPLATES, type TemplateDetail } from '@/constants/templates'
import { useAuthStatus } from '@/hooks/useAuthStatus'
import { useRepoData } from '@/hooks/useRepoData'
@@ -46,10 +46,12 @@ export function ConnectStep() {
// Repository vs Template selection
const [selectedRepo, setSelectedRepo] = useState<string>(
formData.githubRepo || ''
formData.repoName || ''
)
const [selectedTemplate, setSelectedTemplate] = useState(
adaptOptionalTemplate(formData.template)
const [selectedTemplate, setSelectedTemplate] = useState<
TemplateDetail | undefined
>(
undefined // We'll simplify template handling
)
const [projectName, setProjectName] = useState<string>(
formData.projectName || ''
@@ -71,7 +73,23 @@ export function ConnectStep() {
} = useAuthStatus()
// Repository data
const { repoData: repositories, isLoading: isLoadingRepos } = useRepoData('')
const {
repoData: repositories,
isLoading: isLoadingRepos,
error: repoError
} = useRepoData('')
// Debug repository data
useEffect(() => {
console.log('🔍 ConnectStep: Repository data changed:', {
repositories: repositories
? `Array with ${Array.isArray(repositories) ? repositories.length : 'not array'} items`
: 'null',
isLoadingRepos,
repoError,
isFullyAuthenticated
})
}, [repositories, isLoadingRepos, repoError, isFullyAuthenticated])
// Handle hydration mismatch by waiting for mount
useEffect(() => {
@@ -90,9 +108,10 @@ export function ConnectStep() {
setSelectedRepo(repo)
setSelectedTemplate(undefined)
setFormData({
githubRepo: repo,
template: undefined,
deploymentMode: 'repository',
repoName: repo,
githubRepo: repo, // Store repo path for deploy step
template: undefined, // Clear template selection
framework: '', // Clear framework
projectName
})
}
@@ -107,9 +126,10 @@ export function ConnectStep() {
setProjectName(suggestedName)
}
setFormData({
template: template,
githubRepo: '',
deploymentMode: 'template',
framework: template.name, // Keep for backwards compatibility
template: template, // Store the full template object
githubRepo: '', // Clear repo selection
repoName: '',
projectName:
projectName ||
`my-${template.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`
@@ -123,15 +143,17 @@ export function ConnectStep() {
if (mode === 'import') {
setSelectedTemplate(undefined)
setFormData({
framework: '',
template: undefined,
deploymentMode: 'repository',
githubRepo: '',
projectName
})
} else {
setSelectedRepo('')
setFormData({
repoName: '',
githubRepo: '',
deploymentMode: 'template',
template: undefined,
projectName
})
}
@@ -186,9 +208,8 @@ export function ConnectStep() {
// Set final form data and proceed
setFormData({
deploymentMode: isImportMode ? 'repository' : 'template',
githubRepo: isImportMode ? selectedRepo : '',
template: !isImportMode ? (selectedTemplate as Template) : undefined,
repoName: isImportMode ? selectedRepo : '',
framework: !isImportMode ? selectedTemplate?.name || '' : '',
projectName: finalProjectName
})
@@ -1,156 +0,0 @@
// 'use client'
// import { Button } from '@workspace/ui/components/button'
// import { useState } from 'react'
// import { useOnboarding } from '../store'
// type ConnectState = 'initial' | 'repository-select' | 'template-select'
// /**
// * First step in the onboarding flow
// * Handles GitHub connection and repository selection
// *
// * States:
// * - initial: Shows GitHub connect button
// * - repository-select: Shows list of repositories
// * - template-select: Shows available templates
// *
// * @component
// */
// export function ConnectStep() {
// const [connectState, setConnectState] = useState<ConnectState>('initial')
// const [projectName, setProjectName] = useState('')
// const { setFormData, nextStep } = useOnboarding()
// const handleConnect = () => {
// setConnectState('repository-select')
// }
// return (
// <div className="max-w-2xl w-full">
// {/* <ConnectAccountTabPanel />\ */}
// {connectState === 'initial' ? (
// <div className="flex flex-col items-center justify-center gap-6 p-8">
// <h2 className="text-2xl font-semibold text-center">
// Connect to GitHub
// </h2>
// <p className="text-center text-muted-foreground">
// Connect your GitHub account to get started
// </p>
// <Button onClick={handleConnect} />
// </div>
// ) : (
// // <ConnectAccount onAuth={() => {}} />
// <>...connect goes here</>
// )}
// </div>
// )
// }
'use client'
// src/components/onboarding/connect-step/connect-step.tsx
import { useState } from 'react'
import { useRepoData } from '@/hooks/useRepoData'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { ScrollableRepoList } from '@/components/onboarding/connect-step/repository-list'
import { TemplateList } from '@/components/onboarding/connect-step/template-list'
import { StepHeader } from '@/components/onboarding/common/step-header'
import { Button } from '@workspace/ui/components/button'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@workspace/ui/components/select'
import { GitBranch } from 'lucide-react'
export const ConnectStep = () => {
const { setFormData, formData, nextStep } = useOnboarding()
const [selectedRepo, setSelectedRepo] = useState<string>(formData.githubRepo || '')
const [isImportMode, setIsImportMode] = useState(true)
const { repoData: repositories, isLoading } = useRepoData('')
// Handle repository selection
const handleRepoSelect = (repo: string) => {
setSelectedRepo(repo)
setFormData({ githubRepo: repo })
}
// Handle mode toggle between import and template
const toggleMode = (mode: 'import' | 'template') => {
setIsImportMode(mode === 'import')
}
return (
<div className="flex flex-col items-center justify-center w-full px-8">
<div className="w-full max-w-md">
{/* Connect icon */}
<div className="flex justify-center mb-6">
<div className="w-16 h-16 bg-blue-500 bg-opacity-10 rounded-full flex items-center justify-center">
<GitBranch className="h-8 w-8 text-blue-500" />
</div>
</div>
{/* Connect header */}
<StepHeader
title="Connect"
description="Connect and import a GitHub repo or start from a template"
icon={<GitBranch />}
/>
{/* Git account selector */}
<Select defaultValue="git-account">
<SelectTrigger className="mb-4 bg-zinc-900 border-zinc-800">
<SelectValue placeholder="Select Git account" />
</SelectTrigger>
<SelectContent>
<SelectItem value="git-account">git-account</SelectItem>
</SelectContent>
</Select>
{/* Mode buttons */}
<div className="flex mb-4 space-x-2">
<Button
variant={isImportMode ? "default" : "outline"}
className={`flex-1 ${isImportMode ? 'bg-blue-600 hover:bg-blue-700' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800'}`}
onClick={() => toggleMode('import')}
>
Import a repository
</Button>
<Button
variant={!isImportMode ? "default" : "outline"}
className={`flex-1 ${!isImportMode ? 'bg-blue-600 hover:bg-blue-700' : 'bg-zinc-900 border-zinc-800 hover:bg-zinc-800'}`}
onClick={() => toggleMode('template')}
>
Start with a template
</Button>
</div>
{/* Repository list or template list */}
{isImportMode ? (
<ScrollableRepoList
repositories={repositories || []}
isLoading={isLoading}
onSelect={handleRepoSelect}
selectedRepo={selectedRepo}
/>
) : (
<TemplateList onSelect={() => {}} />
)}
{/* Navigation buttons */}
<div className="flex justify-end space-x-2 mt-8">
<Button
variant="ghost"
className="text-muted-foreground bg-zinc-900 border-zinc-800 hover:bg-zinc-800"
disabled={true} // Disable previous on first step
>
Previous
</Button>
<Button
className="bg-blue-600 hover:bg-blue-700"
onClick={nextStep}
disabled={!selectedRepo && isImportMode}
>
Next
</Button>
</div>
</div>
</div>
)
}
@@ -49,16 +49,16 @@
import { Github } from 'lucide-react'
interface Repository {
id: string | number;
full_name: string;
html_url?: string;
id: string | number
full_name: string
html_url?: string
}
interface ScrollableRepoListProps {
repositories: Repository[];
isLoading: boolean;
onSelect: (repoFullName: string) => void;
selectedRepo: string;
repositories: Repository[]
isLoading: boolean
onSelect: (repoFullName: string) => void
selectedRepo: string
}
export function ScrollableRepoList({
@@ -72,14 +72,30 @@ export function ScrollableRepoList({
return (
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
<div className="p-6 text-center text-zinc-400">
<svg className="animate-spin h-5 w-5 text-zinc-400 mx-auto mb-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg
className="animate-spin h-5 w-5 text-zinc-400 mx-auto mb-2"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Loading repositories...
</div>
</div>
);
)
}
if (!repositories || repositories.length === 0) {
@@ -89,29 +105,31 @@ export function ScrollableRepoList({
No repositories found. Make sure your GitHub account is connected.
</div>
</div>
);
)
}
// Generate mock repositories if needed for testing
const repoList = repositories.length > 0 ? repositories :
Array.from({ length: 15 }, (_, i) => ({
id: `mock-${i}`,
full_name: `git-account/repo-name-${i + 1}`
}));
const repoList =
repositories.length > 0
? repositories
: Array.from({ length: 15 }, (_, i) => ({
id: `mock-${i}`,
full_name: `git-account/repo-name-${i + 1}`
}))
return (
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
{/* Ensure we have a fixed height container with scrolling */}
<div
<div
className="max-h-60 overflow-y-auto"
style={{
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#4b5563 #1f2937'
}}
>
{repoList.map((repo, index) => (
<div
key={repo.id || index}
<div
key={repo.id || index}
className={`flex items-center p-4 border-b border-zinc-800 last:border-b-0 cursor-pointer hover:bg-zinc-800 transition-colors duration-150 ${
selectedRepo === repo.full_name ? 'bg-zinc-800' : ''
}`}
@@ -121,12 +139,10 @@ export function ScrollableRepoList({
<Github className="inline-block h-4 w-4 mr-2 text-zinc-400" />
<span>{repo.full_name}</span>
</div>
<div className="text-sm text-zinc-400">
5 minutes ago
</div>
<div className="text-sm text-zinc-400">5 minutes ago</div>
</div>
))}
</div>
</div>
);
}
)
}
@@ -66,18 +66,21 @@
import { Box, Layout } from 'lucide-react'
interface Template {
id: string;
name: string;
description: string;
icon: React.ReactNode;
id: string
name: string
description: string
icon: React.ReactNode
}
interface TemplateListProps {
onSelect: (templateId: string) => void;
selectedTemplate?: string;
onSelect: (templateId: string) => void
selectedTemplate?: string
}
export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps) => {
export const TemplateList = ({
onSelect,
selectedTemplate
}: TemplateListProps) => {
// Mock templates data - in a real app, this would come from an API
const templates: Template[] = [
{
@@ -98,7 +101,7 @@ export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps)
description: 'Simple static site with HTML, CSS, and JavaScript',
icon: <Layout className="h-6 w-6" />
}
];
]
return (
<div className="border border-zinc-800 rounded-md bg-zinc-900 overflow-hidden">
@@ -116,11 +119,13 @@ export const TemplateList = ({ onSelect, selectedTemplate }: TemplateListProps)
</div>
<div className="flex-1">
<div className="font-medium text-sm">{template.name}</div>
<div className="text-xs text-muted-foreground">{template.description}</div>
<div className="text-xs text-muted-foreground">
{template.description}
</div>
</div>
</div>
))}
</div>
</div>
);
};
)
}
@@ -1,9 +1,10 @@
// src/components/onboarding/deploy-step/deploy-step.tsx
'use client'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { useOnboarding } from '@/components/onboarding/store'
import { useWallet } from '@/context/WalletContext'
import { useDeployment } from '@/hooks/useDeployment'
import { useDirectTemplateDeployment } from '@/hooks/useDirectTemplateDeployment'
import { useTemplateDeployment } from '@/hooks/useTemplate'
import { Alert, AlertDescription } from '@workspace/ui/components/alert'
import { Badge } from '@workspace/ui/components/badge'
@@ -44,7 +45,7 @@ export function DeployStep() {
useTemplateDeployment()
// Determine deployment type and get the right deploying state
const isTemplateMode = formData.deploymentMode === 'template'
const isTemplateMode = formData.template ? true : false
const isDeploying = isTemplateMode ? isTemplateDeploying : isRepoDeploying
// Handle hydration mismatch by waiting for mount
@@ -132,11 +133,17 @@ export function DeployStep() {
throw new Error('Missing required template deployment data')
}
if (!formData.template.repoFullName) {
throw new Error('Template repository information is missing')
}
const config = {
template: {
...formData.template,
id: formData.template?.id || '',
icon: formData.template?.icon || ''
id: formData.template.id,
name: formData.template.name,
icon: formData.template.icon || '',
repoFullName: formData.template.repoFullName,
description: formData.template.description || ''
},
projectName: formData.projectName,
organizationSlug: formData.selectedOrg,
@@ -1,42 +0,0 @@
'use client'
import { useOnboarding } from '../store'
/**
* Final step in the onboarding flow
* Displays deployment summary and triggers deployment
*
* Features:
* - Configuration summary
* - Repository display
* - Deploy action
*
* @component
*/
export function DeployStep() {
useOnboarding()
return (
<div className="w-full">
<div className="max-w-2xl mx-auto space-y-8">
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section */}
<div className="flex flex-col items-center gap-1">
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
<p className="text-base text-muted-foreground text-center">
Your deployment is configured and ready to go!
</p>
</div>
</div>
Content sections will be placed here: 1. Repository info card 2.
Configuration summary 3. Deploy button
{/* ...content here */}
{/* <Deploy /> */}
</div>
</div>
</div>
</div>
)
}
@@ -7,6 +7,7 @@
'use client'
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import type { OnboardingFormData, Step } from './types'
/**
@@ -18,6 +19,7 @@ import type { OnboardingFormData, Step } from './types'
* @property {(data: Partial<OnboardingFormData>) => void} setFormData - Updates form data
* @property {() => void} nextStep - Moves to the next step
* @property {() => void} previousStep - Moves to the previous step
* @property {() => void} resetOnboarding - Resets the onboarding state to initial values
*/
export interface OnboardingState {
currentStep: Step
@@ -26,10 +28,33 @@ export interface OnboardingState {
setFormData: (data: Partial<OnboardingFormData>) => void
nextStep: () => void
previousStep: () => void
resetOnboarding: () => void
}
/** Order of steps in the onboarding flow */
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy', 'success']
/** Initial form data values */
const initialFormData: OnboardingFormData = {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: '',
template: undefined,
githubRepo: '',
selectedOrg: '',
environmentVariables: [],
selectedLrn: '',
deploymentType: 'lrn',
maxPrice: '1000',
deployerCount: '1',
deploymentId: undefined,
deploymentUrl: undefined,
projectId: undefined,
repositoryUrl: undefined
}
/**
* Zustand store for managing onboarding state
@@ -40,31 +65,58 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
* const { currentStep, formData, nextStep } = useOnboarding()
* ```
*/
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: 'connect',
formData: {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: ''
},
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) =>
set((state) => ({
formData: { ...state.formData, ...data }
})),
nextStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const nextStep = STEP_ORDER[currentIndex + 1]
return nextStep ? { currentStep: nextStep } : state
export const useOnboarding = create<OnboardingState>()(
persist(
(set) => ({
currentStep: 'connect',
formData: initialFormData,
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) => {
set((state) => {
const newFormData = { ...state.formData, ...data }
// Keep organizationSlug and selectedOrg in sync
if (data.organizationSlug && !data.selectedOrg) {
newFormData.selectedOrg = data.organizationSlug
}
if (data.selectedOrg && !data.organizationSlug) {
newFormData.organizationSlug = data.selectedOrg
}
console.log('Store: Setting form data:', data)
console.log('Store: New state:', newFormData)
return { formData: newFormData }
})
},
nextStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const nextStep = STEP_ORDER[currentIndex + 1]
return nextStep ? { currentStep: nextStep } : state
}),
previousStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const previousStep = STEP_ORDER[currentIndex - 1]
return previousStep ? { currentStep: previousStep } : state
}),
resetOnboarding: () => {
set({ currentStep: 'connect', formData: initialFormData })
}
}),
previousStep: () =>
set((state) => {
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const previousStep = STEP_ORDER[currentIndex - 1]
return previousStep ? { currentStep: previousStep } : state
})
}))
{
name: 'laconic-onboarding',
storage: createJSONStorage(() => localStorage),
// Only persist formData, not currentStep
partialize: (state) => ({
formData: state.formData
})
}
)
)
@@ -1,11 +1,11 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter, useParams } from 'next/navigation'
import { useTheme } from 'next-themes'
import { CheckCircle } from 'lucide-react'
import { useOnboarding } from '@/components/onboarding/store'
import { Button } from '@workspace/ui/components/button'
import { useOnboarding } from '@/components/onboarding/useOnboarding'
import { CheckCircle } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useState } from 'react'
export function SuccessStep() {
const router = useRouter()
@@ -14,45 +14,49 @@ export function SuccessStep() {
const [mounted, setMounted] = useState(false)
const providerParam = params?.provider ? String(params.provider) : 'github'
const { formData, resetOnboarding } = useOnboarding()
// Handle hydration mismatch by waiting for mount
useEffect(() => {
setMounted(true)
}, [])
// Get deployment info from form data
const repoName = formData.githubRepo ? formData.githubRepo.split('/').pop() : (formData.projectName || 'project')
const deploymentUrl = formData.deploymentUrl || `https://${repoName}.laconic.deploy`
const projectId = formData.projectId || 'unknown-id'
// Get deployment info from form data - using available properties
const repoName = formData.githubRepo
? formData.githubRepo.split('/').pop()
: formData.repoName
? formData.repoName.split('/').pop()
: formData.projectName || 'project'
const deploymentUrl = `https://${repoName}.laconic.deploy` // Default deployment URL
const projectId = formData.projectId || formData.deploymentId || 'unknown-id' // Use projectId first, fallback to deploymentId
// Handle "View Project" button - navigates to project page
const handleViewProject = () => {
console.log('Navigating to project with ID:', projectId)
resetOnboarding() // Reset state for next time
// Navigate to the project detail page using the GraphQL project ID
router.push(`/projects/${providerParam}/ps/${projectId}`)
}
// Auto-redirect after a delay (optional)
useEffect(() => {
if (mounted && projectId && projectId !== 'unknown-id') {
const timer = setTimeout(() => {
handleViewProject()
}, 3000) // Auto-redirect after 3 seconds
return () => clearTimeout(timer)
}
}, [mounted, projectId])
// Don't render UI until after mount to prevent hydration mismatch
if (!mounted) {
return null
}
// Determine if dark mode is active
const isDarkMode = resolvedTheme === 'dark'
return (
<div className="w-full h-full flex flex-col items-center justify-center p-8">
<div className="max-w-md w-full mx-auto">
@@ -60,17 +64,21 @@ export function SuccessStep() {
<div className="mx-auto mb-6 flex justify-center">
<CheckCircle className="h-16 w-16 text-green-500" />
</div>
{/* Success header */}
<h2 className={`text-2xl font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} text-center mb-2`}>
<h2
className={`text-2xl font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} text-center mb-2`}
>
Successfully Deployed!
</h2>
<p className="text-center text-zinc-500 mb-8">
Your project has been deployed successfully
</p>
{/* Deployment summary */}
<div className={`border rounded-md p-4 mb-6 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
<div
className={`border rounded-md p-4 mb-6 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-muted-foreground">Project:</span>
@@ -86,39 +94,82 @@ export function SuccessStep() {
</div>
</div>
</div>
{/* Next steps section */}
<div className="mb-8">
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
<div className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
<h3
className={`text-lg font-medium ${isDarkMode ? 'text-white' : 'text-zinc-900'} mb-4`}
>
Next steps
</h3>
<div
className={`border rounded-md overflow-hidden mb-4 ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<div className="flex items-center p-4 justify-between">
<div>
<div className={isDarkMode ? "text-white font-medium" : "text-zinc-900 font-medium"}>Setup Domain</div>
<div className="text-zinc-500 text-sm">Add a custom domain to your project.</div>
<div
className={
isDarkMode
? 'text-white font-medium'
: 'text-zinc-900 font-medium'
}
>
Setup Domain
</div>
<div className="text-zinc-500 text-sm">
Add a custom domain to your project.
</div>
</div>
<Button variant="outline" className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? "border-zinc-700" : "border-zinc-300"}`}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={isDarkMode ? "text-white" : "text-zinc-900"}>
<path d="M9 18L15 12L9 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<Button
variant="outline"
className={`rounded-full p-1 w-8 h-8 flex items-center justify-center ${isDarkMode ? 'border-zinc-700' : 'border-zinc-300'}`}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={isDarkMode ? 'text-white' : 'text-zinc-900'}
>
<path
d="M9 18L15 12L9 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
</div>
</div>
</div>
{/* Action buttons */}
<div className="flex flex-col space-y-3">
<Button
<Button
className="w-full bg-white hover:bg-white/90 text-black flex items-center justify-center"
onClick={handleViewProject}
disabled={!projectId || projectId === 'unknown-id'}
>
View Project
<svg className="ml-2 h-4 w-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19M19 12L13 6M19 12L13 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
View Project
<svg
className="ml-2 h-4 w-4"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 12H19M19 12L13 6M19 12L13 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Button>
{/* Manual navigation button if auto-redirect fails */}
{projectId === 'unknown-id' && (
<p className="text-center text-xs text-muted-foreground">
@@ -129,4 +180,4 @@ export function SuccessStep() {
</div>
</div>
)
}
}
@@ -18,6 +18,18 @@ export type Step = 'connect' | 'configure' | 'deploy' | 'success'
* @property {string} framework - Framework used for the project
* @property {string} access - Access level of the repository
* @property {string} organizationSlug - Organization slug
* @property {Template | undefined} template - Selected template for deployment
* @property {string} githubRepo - GitHub repository path (owner/repo)
* @property {string} selectedOrg - Selected organization for deployment
* @property {EnvironmentVariable[]} environmentVariables - Environment variables for deployment
* @property {string} selectedLrn - Selected LRN for deployment
* @property {string} deploymentType - Type of deployment (lrn, wallet, etc.)
* @property {string} maxPrice - Maximum price for deployment
* @property {string} deployerCount - Number of deployers
* @property {string} deploymentId - ID of the deployment after creation
* @property {string} deploymentUrl - URL of the deployed project
* @property {string} projectId - ID of the created project
* @property {string} repositoryUrl - URL of the repository
*/
export interface OnboardingFormData {
projectName: string
@@ -26,6 +38,18 @@ export interface OnboardingFormData {
framework: string
access: 'public' | 'private'
organizationSlug: string
template?: Template
githubRepo: string
selectedOrg: string
environmentVariables: EnvironmentVariable[]
selectedLrn: string
deploymentType: string
maxPrice: string
deployerCount: string
deploymentId?: string
deploymentUrl?: string
projectId?: string
repositoryUrl?: string
}
/**
@@ -54,12 +78,16 @@ export interface Repository {
* @property {string} name - Template name
* @property {string} [description] - Template description
* @property {string} [thumbnail] - Template thumbnail URL
* @property {string} [repoFullName] - Full repository name for the template
* @property {any} [icon] - Template icon
*/
export interface Template {
id: string
name: string
description?: string
thumbnail?: string
repoFullName?: string
icon?: any
}
/**
@@ -81,9 +109,11 @@ export interface DeploymentType {
* @property {string} key - Environment variable key
* @property {string} value - Environment variable value
* @property {boolean} [isSecret] - Whether the variable is a secret
* @property {string[]} environments - Environment names where this variable applies
*/
export interface EnvironmentVariable {
key: string
value: string
isSecret?: boolean
environments: string[]
}
@@ -1,120 +0,0 @@
// src/components/onboarding/useOnboarding.ts
import { useState } from 'react'
import type {
OnboardingFormData} from '@/types'
// Step type for navigation
export type Step = 'connect' | 'configure' | 'deploy' | 'success'
// Initial form data
const initialFormData: OnboardingFormData = {
// Step 1: Connect
githubRepo: undefined,
template: undefined,
projectName: undefined,
deploymentMode: undefined,
// Step 2: Configure
selectedLrn: undefined,
selectedOrg: undefined, // Add this property
environmentVariables: [], // Use array type
deploymentType: undefined, // Add missing properties
deployerCount: undefined,
maxPrice: undefined,
// Step 3: Deploy
deploymentId: undefined,
repositoryUrl: undefined,
deploymentUrl: undefined, // Add missing property
projectId: undefined // Add missing property
}
export function useOnboarding() {
const [formData, setFormData] = useState<OnboardingFormData>(initialFormData)
const [currentStep, setCurrentStep] = useState<Step>('connect')
// Update form data (partial update)
const updateFormData = (updates: Partial<OnboardingFormData>) => {
setFormData(prev => ({ ...prev, ...updates }))
}
// Reset form data
const resetFormData = () => {
setFormData(initialFormData)
setCurrentStep('connect')
}
// Step navigation
const nextStep = () => {
switch (currentStep) {
case 'connect':
setCurrentStep('configure')
break
case 'configure':
setCurrentStep('deploy')
break
case 'deploy':
setCurrentStep('success')
break
}
}
const previousStep = () => {
switch (currentStep) {
case 'configure':
setCurrentStep('connect')
break
case 'deploy':
setCurrentStep('configure')
break
case 'success':
setCurrentStep('deploy')
break
}
}
// Validation helpers
const validateStep1 = () => {
if (formData.deploymentMode === 'template') {
return !!(formData.template && formData.projectName)
}
return !!formData.githubRepo
}
const validateStep2 = () => {
return !!(formData.selectedLrn && formData.selectedOrg)
}
const validateStep3 = () => {
return !!formData.deploymentId
}
return {
// Data
formData,
currentStep,
// Actions
setFormData: updateFormData,
setCurrentStep,
resetFormData,
resetOnboarding: resetFormData, // Alias for compatibility
nextStep,
previousStep,
// Validation
validateStep1,
validateStep2,
validateStep3,
// Store-like interface for compatibility
getState: () => ({
formData,
currentStep,
nextStep,
previousStep,
setFormData: updateFormData,
resetFormData
})
}
}
+86 -47
View File
@@ -1,11 +1,11 @@
// src/hooks/useAuthStatus.tsx
'use client'
import { useAuth, useUser } from '@clerk/nextjs'
import { useWallet } from '@/context/WalletContext' // Use the full provider!
import { useBackend } from '@/context/BackendContext'
import { useGQLClient } from '@/context'
import { useState, useEffect, useCallback } from 'react'
import { useBackend } from '@/context/BackendContext'
import { useWallet } from '@/context/WalletContext' // Use the full provider!
import { useAuth, useUser } from '@clerk/nextjs'
import { useCallback, useEffect, useRef, useState } from 'react'
/**
* @interface AuthStatus
@@ -20,8 +20,8 @@ export interface AuthStatus {
user: any
}
wallet: {
isConnected: boolean // SIWE authenticated + backend session
hasAddress: boolean // Just has wallet address
isConnected: boolean // SIWE authenticated + backend session
hasAddress: boolean // Just has wallet address
wallet: any
}
backend: {
@@ -29,11 +29,11 @@ export interface AuthStatus {
hasGithubAuth: boolean
isLoading: boolean
}
// Computed status
isFullyAuthenticated: boolean
isReady: boolean
// What's missing (for UI feedback)
missing: {
clerkSignIn: boolean
@@ -42,7 +42,7 @@ export interface AuthStatus {
backendConnection: boolean
githubBackendSync: boolean
}
// Progress (for UI indicators)
progress: {
completed: number
@@ -58,7 +58,7 @@ export interface AuthStatus {
export interface AuthActions {
// Wallet actions
connectWallet: () => Promise<void>
// Combined actions
refreshAllStatus: () => Promise<void>
checkGithubBackendAuth: () => Promise<boolean>
@@ -73,35 +73,52 @@ export function useAuthStatus(): AuthStatus & AuthActions {
// Clerk authentication
const { isSignedIn, isLoaded: isClerkLoaded } = useAuth()
const { user, isLoaded: isUserLoaded } = useUser()
// Wallet authentication
const {
isConnected: isWalletSessionActive, // SIWE authenticated
hasWalletAddress,
wallet,
connect: connectWallet
const {
isConnected: isWalletSessionActive, // SIWE authenticated
hasWalletAddress,
wallet,
connect: connectWallet
} = useWallet()
// Backend authentication
const {
isBackendConnected,
isLoading: isBackendLoading,
refreshStatus: refreshBackendStatus
} = useBackend()
// GraphQL client for checking GitHub backend auth
const gqlClient = useGQLClient()
// GitHub backend auth state
const [isGithubBackendAuth, setIsGithubBackendAuth] = useState(false)
const [isCheckingGithubAuth, setIsCheckingGithubAuth] = useState(false)
const lastGithubCheckRef = useRef(0)
const isCheckingRef = useRef(false)
// Stable status to prevent rapid UI changes
const [stableAuthStatus, setStableAuthStatus] = useState({
isFullyAuthenticated: false,
lastUpdate: 0
})
// Check GitHub backend auth via GraphQL
const checkGithubBackendAuth = useCallback(async (): Promise<boolean> => {
if (!isBackendConnected) return false
// Prevent multiple rapid calls - only allow once every 3 seconds
const now = Date.now()
if (isCheckingRef.current || (now - lastGithubCheckRef.current < 3000)) {
return isGithubBackendAuth
}
try {
isCheckingRef.current = true
setIsCheckingGithubAuth(true)
lastGithubCheckRef.current = now
const userData = await gqlClient.getUser()
const hasGitHubToken = !!userData.user.gitHubToken
setIsGithubBackendAuth(hasGitHubToken)
@@ -111,10 +128,11 @@ export function useAuthStatus(): AuthStatus & AuthActions {
setIsGithubBackendAuth(false)
return false
} finally {
isCheckingRef.current = false
setIsCheckingGithubAuth(false)
}
}, [isBackendConnected, gqlClient])
}, [isBackendConnected, isGithubBackendAuth]) // Minimal dependencies
// Check GitHub auth when backend connection changes
useEffect(() => {
if (isBackendConnected) {
@@ -122,9 +140,9 @@ export function useAuthStatus(): AuthStatus & AuthActions {
} else {
setIsGithubBackendAuth(false)
}
}, [isBackendConnected, checkGithubBackendAuth])
// Check backend connection when wallet session is active (SIWE completed)
}, [isBackendConnected]) // Remove checkGithubBackendAuth from dependencies to prevent infinite loop
// Check backend connection when wallet session is active (SIWE completed)
useEffect(() => {
if (isWalletSessionActive) {
// Wait a moment for wallet session to be established, then check backend
@@ -133,13 +151,16 @@ export function useAuthStatus(): AuthStatus & AuthActions {
}, 1000)
return () => clearTimeout(timer)
}
}, [isWalletSessionActive, refreshBackendStatus])
}, [isWalletSessionActive]) // Remove refreshBackendStatus from dependencies to prevent rapid retriggers
// Check if GitHub is connected in Clerk
const hasGithubInClerk = user?.externalAccounts?.find(
account => account.provider === 'github' || account.verification?.strategy === 'oauth_github'
) !== undefined
const hasGithubInClerk =
user?.externalAccounts?.find(
(account) =>
account.provider === 'github' ||
account.verification?.strategy === 'oauth_github'
) !== undefined
// Calculate what's missing
const missing = {
clerkSignIn: !isSignedIn,
@@ -148,32 +169,50 @@ export function useAuthStatus(): AuthStatus & AuthActions {
backendConnection: hasWalletAddress && !isWalletSessionActive, // Need SIWE auth for backend
githubBackendSync: isBackendConnected && !isGithubBackendAuth
}
// Calculate progress
const authSteps = [
isSignedIn, // Clerk sign in
hasGithubInClerk, // GitHub connected to Clerk
hasWalletAddress, // Wallet address obtained
isWalletSessionActive, // SIWE authentication completed
isGithubBackendAuth // GitHub synced to backend
isSignedIn, // Clerk sign in
hasGithubInClerk, // GitHub connected to Clerk
hasWalletAddress, // Wallet address obtained
isWalletSessionActive, // SIWE authentication completed
isGithubBackendAuth // GitHub synced to backend
]
const completedSteps = authSteps.filter(Boolean).length
const totalSteps = authSteps.length
const progressPercentage = Math.round((completedSteps / totalSteps) * 100)
// Determine if fully authenticated
const isFullyAuthenticated = authSteps.every(Boolean)
const currentIsFullyAuthenticated = authSteps.every(Boolean)
// Debounce authentication status changes to prevent flickering
useEffect(() => {
const now = Date.now()
const timeSinceLastUpdate = now - stableAuthStatus.lastUpdate
// Only update if status actually changed and enough time has passed (300ms debounce)
if (currentIsFullyAuthenticated !== stableAuthStatus.isFullyAuthenticated && timeSinceLastUpdate > 300) {
setStableAuthStatus({
isFullyAuthenticated: currentIsFullyAuthenticated,
lastUpdate: now
})
}
}, [currentIsFullyAuthenticated, stableAuthStatus])
// Use stable status for UI
const isFullyAuthenticated = stableAuthStatus.isFullyAuthenticated
// Determine if ready (all auth systems loaded)
const isReady = isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
const isReady =
isClerkLoaded && isUserLoaded && !isBackendLoading && !isCheckingGithubAuth
// Combined refresh action
const refreshAllStatus = async () => {
await refreshBackendStatus()
await checkGithubBackendAuth()
}
return {
// Individual systems
clerk: {
@@ -192,24 +231,24 @@ export function useAuthStatus(): AuthStatus & AuthActions {
hasGithubAuth: isGithubBackendAuth,
isLoading: isBackendLoading || isCheckingGithubAuth
},
// Computed status
isFullyAuthenticated,
isReady,
// Missing items
missing,
// Progress
progress: {
completed: completedSteps,
total: totalSteps,
percentage: progressPercentage
},
// Actions
connectWallet,
refreshAllStatus,
checkGithubBackendAuth
}
}
}
@@ -0,0 +1,118 @@
'use client'
import { getGitHubToken } from '@/actions/github'
import { Octokit } from '@octokit/rest'
import { useState } from 'react'
import { toast } from 'sonner'
interface CreateRepoFromTemplateParams {
templateOwner: string
templateRepo: string
name: string
description?: string
isPrivate?: boolean
}
interface CreateRepoResult {
success: boolean
repositoryUrl?: string
error?: string
}
/**
* Hook to directly interact with GitHub API using user's own token
* Bypasses backend GitHub integration issues
*/
export function useDirectGitHub() {
const [isLoading, setIsLoading] = useState(false)
const createRepoFromTemplate = async (
params: CreateRepoFromTemplateParams
): Promise<CreateRepoResult> => {
setIsLoading(true)
try {
console.log(
'🔄 Creating repository from template directly via GitHub API...'
)
console.log('📋 Parameters:', params)
// Get user's GitHub token
const token = await getGitHubToken()
if (!token) {
throw new Error(
'GitHub token not available. Please reconnect your GitHub account.'
)
}
// Create Octokit instance with user's token
const octokit = new Octokit({ auth: token })
// Get the authenticated user's info to use as owner
const { data: authUser } = await octokit.rest.users.getAuthenticated()
console.log('👤 Authenticated GitHub user:', authUser.login)
// Create repository from template
const { data: newRepo } = await octokit.rest.repos.createUsingTemplate({
template_owner: params.templateOwner,
template_repo: params.templateRepo,
owner: authUser.login, // Use authenticated user as owner
name: params.name,
description:
params.description ||
`Created from ${params.templateOwner}/${params.templateRepo}`,
private: params.isPrivate || false,
include_all_branches: false
})
console.log('✅ Repository created successfully:', newRepo.html_url)
return {
success: true,
repositoryUrl: newRepo.html_url
}
} catch (error) {
console.error('❌ Failed to create repository from template:', error)
let errorMessage = 'Failed to create repository from template'
if (error instanceof Error) {
errorMessage = error.message
}
return {
success: false,
error: errorMessage
}
} finally {
setIsLoading(false)
}
}
const getUserRepos = async () => {
try {
const token = await getGitHubToken()
if (!token) {
throw new Error('GitHub token not available')
}
const octokit = new Octokit({ auth: token })
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser(
{
sort: 'updated',
per_page: 100
}
)
return repos
} catch (error) {
console.error('❌ Failed to fetch user repositories:', error)
throw error
}
}
return {
createRepoFromTemplate,
getUserRepos,
isLoading
}
}
@@ -0,0 +1,316 @@
// src/hooks/useDirectTemplateDeployment.tsx
'use client'
import { useState } from 'react'
import { useGQLClient } from '@/context'
import { useWallet } from '@/context/WalletContext'
import { useUser } from '@clerk/nextjs'
import { toast } from 'sonner'
import type { TemplateDetail } from '@/constants/templates'
import { useDirectGitHub } from './useDirectGitHub'
import { getGitHubToken } from '@/actions/github'
export interface TemplateDeploymentConfig {
template: TemplateDetail
projectName: string
organizationSlug: string
environmentVariables?: Array<{
key: string
value: string
environments: string[]
}>
deployerLrn?: string
}
export interface TemplateDeploymentResult {
projectId: string
repositoryUrl: string
deploymentUrl?: string
deploymentId?: string
}
export function useDirectTemplateDeployment() {
const [isDeploying, setIsDeploying] = useState(false)
const [deploymentResult, setDeploymentResult] =
useState<TemplateDeploymentResult | null>(null)
const [error, setError] = useState<string | null>(null)
const gqlClient = useGQLClient()
const { wallet } = useWallet()
const { user } = useUser()
const directGitHub = useDirectGitHub()
const deployTemplate = async (
config: TemplateDeploymentConfig
): Promise<TemplateDeploymentResult> => {
setIsDeploying(true)
setError(null)
setDeploymentResult(null)
try {
console.log('🚀 Starting direct template deployment:', config)
// Validate required data
if (!wallet?.address) {
throw new Error('Wallet not connected')
}
if (!user) {
throw new Error('User not authenticated')
}
// Get GitHub username from Clerk external accounts
const githubAccount = user.externalAccounts?.find(
(account) => account.provider === 'github'
)
const githubUsername = githubAccount?.username
if (!githubUsername) {
throw new Error('GitHub account not connected')
}
console.log('🔍 GitHub user info:', {
githubUsername,
githubAccount: githubAccount?.username,
userExternalAccounts: user.externalAccounts?.length
})
// Parse template repository (format: "owner/repo")
const [templateOwner, templateRepo] =
config.template.repoFullName.split('/')
if (!templateOwner || !templateRepo) {
throw new Error('Invalid template repository format')
}
console.log('🔍 Template parsing details:', {
originalTemplate: config.template.repoFullName,
parsedOwner: templateOwner,
parsedRepo: templateRepo,
templateId: config.template.id,
templateName: config.template.name
})
toast.info('Creating repository from template...')
// STEP 1: Create repository directly via GitHub API with user's token
const repoResult = await directGitHub.createRepoFromTemplate({
templateOwner,
templateRepo,
name: config.projectName,
description: `Created from ${config.template.name} template`,
isPrivate: false
})
if (!repoResult.success || !repoResult.repositoryUrl) {
throw new Error(
repoResult.error || 'Failed to create repository from template'
)
}
console.log(
'✅ Repository created successfully:',
repoResult.repositoryUrl
)
toast.success('Repository created from template!')
// STEP 2: Create project in backend using the newly created repository
console.log('🔍 Preparing backend project creation...')
console.log('🔍 Organization slug:', config.organizationSlug)
console.log('🔍 Deployer LRN:', config.deployerLrn || 'undefined')
console.log(
'🔍 Environment variables:',
config.environmentVariables || []
)
toast.info('Setting up project deployment...')
// Add a delay to ensure the repository is fully created and accessible
console.log('⏳ Waiting for repository to be fully accessible...')
await new Promise((resolve) => setTimeout(resolve, 5000))
// STEP 2a: Verify GitHub token availability
console.log('🔄 Verifying GitHub token for user:', githubUsername)
const currentClerkToken = await getGitHubToken()
if (!currentClerkToken) {
throw new Error(
`GitHub token not available for user ${githubUsername}. Please reconnect your GitHub account.`
)
}
console.log('✅ GitHub token verified for user:', githubUsername)
try {
// Get available deployers
console.log('🔍 Fetching available deployers...')
const deployersResult = await gqlClient.getDeployers()
const availableDeployers = deployersResult.deployers || []
if (availableDeployers.length === 0) {
throw new Error(
'No deployers available. Please configure at least one deployer in the backend.'
)
}
// Use the first available deployer if none specified
const deployerToUse =
config.deployerLrn || availableDeployers[0]?.deployerLrn
if (!deployerToUse) {
throw new Error('No valid deployer found')
}
console.log('🔍 Using deployer:', deployerToUse)
// Get the backend's wallet address for blockchain transactions
const backendAddress = await gqlClient.getAddress()
console.log('🔍 Backend wallet address:', backendAddress)
console.log('🔍 Frontend wallet address:', wallet.address)
// Use backend's address for blockchain transactions
const projectData = {
name: config.projectName,
repository: `${githubUsername}/${config.projectName}`,
prodBranch: 'main',
template: config.template.id,
paymentAddress: backendAddress,
txHash:
'0x0000000000000000000000000000000000000000000000000000000000000000'
}
console.log(
'📤 Final project data being sent:',
JSON.stringify(projectData, null, 2)
)
console.log('📤 With deployer:', deployerToUse)
console.log('📤 Organization slug:', config.organizationSlug)
console.log(
'📤 Environment variables:',
config.environmentVariables || []
)
// Log the exact GraphQL variables being sent
const mutationVariables = {
organizationSlug: config.organizationSlug,
data: projectData,
lrn: deployerToUse,
auctionParams: undefined,
environmentVariables: config.environmentVariables || []
}
console.log(
'🔍 EXACT GraphQL variables being sent:',
JSON.stringify(mutationVariables, null, 2)
)
const projectResult = await gqlClient.addProject(
config.organizationSlug,
projectData,
deployerToUse,
undefined, // auctionParams
config.environmentVariables || []
)
console.log('✅ Backend response received:', projectResult)
console.log('🔍 Project ID:', projectResult.addProject?.id)
console.log('🔍 Full project object:', projectResult.addProject)
if (!projectResult.addProject?.id) {
console.error(
'❌ No project ID in response. Full response:',
projectResult
)
throw new Error(
'Failed to set up project deployment in backend - no project ID returned'
)
}
console.log(
'✅ Project created successfully with ID:',
projectResult.addProject.id
)
// Create and return the result
const result: TemplateDeploymentResult = {
projectId: projectResult.addProject.id,
repositoryUrl: repoResult.repositoryUrl,
deploymentUrl: undefined, // Will be populated once deployment completes
deploymentId: projectResult.addProject.id
}
setDeploymentResult(result)
toast.success('Template deployed successfully!')
return result
} catch (backendError) {
console.error('❌ Backend project creation failed:', backendError)
// Enhanced error handling
let errorMessage = 'Unknown deployment error'
let errorDetails = {
message: 'Unknown error',
stack: undefined as string | undefined,
name: 'UnknownError'
}
if (backendError instanceof Error) {
errorMessage = backendError.message
errorDetails = {
message: backendError.message,
stack: backendError.stack,
name: backendError.name
}
} else if (typeof backendError === 'object' && backendError !== null) {
const errorObj = backendError as any
errorMessage = errorObj.message || JSON.stringify(backendError)
errorDetails = {
message: errorObj.message || errorMessage,
stack: errorObj.stack,
name: errorObj.name || 'BackendError'
}
}
console.error('❌ Error details:', errorDetails)
// Provide more specific error messages
if (
errorMessage.includes('Cannot return null for non-nullable field')
) {
errorMessage = `Backend validation error: A required field is missing or invalid.
Possible issues:
1. Invalid template ID: "${config.template.id}"
2. Repository format issue: "${repoResult.repositoryUrl}"
3. Missing deployer configuration
4. Organization "${config.organizationSlug}" not found
Repository was created successfully: ${repoResult.repositoryUrl}
You may need to check the backend configuration or contact support.`
}
throw new Error(`Backend deployment setup failed: ${errorMessage}`)
}
} catch (error) {
console.error('❌ Template deployment failed:', error)
const errorMessage =
error instanceof Error ? error.message : 'Unknown error'
setError(errorMessage)
toast.error(`Template deployment failed: ${errorMessage}`)
throw error
} finally {
setIsDeploying(false)
}
}
const reset = () => {
setDeploymentResult(null)
setError(null)
setIsDeploying(false)
}
return {
deployTemplate,
isDeploying: isDeploying || directGitHub.isLoading,
deploymentResult,
error,
reset
}
}
@@ -0,0 +1,69 @@
'use client'
import { getGitHubToken } from '@/actions/github'
import { useAuth } from '@clerk/nextjs'
import { useEffect, useState } from 'react'
interface UseGitHubTokenReturn {
token: string | null
isLoading: boolean
error: string | null
refreshToken: () => Promise<void>
}
/**
* A hook to get the current user's GitHub OAuth token from Clerk
* This ensures each user gets their own token for GitHub API calls
*/
export function useGitHubToken(): UseGitHubTokenReturn {
const [token, setToken] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { isLoaded: isAuthLoaded, userId } = useAuth()
const fetchToken = async () => {
if (!userId) {
setError('User not authenticated')
setIsLoading(false)
return
}
try {
setIsLoading(true)
setError(null)
const userToken = await getGitHubToken()
if (!userToken) {
setError(
'GitHub account not connected. Please connect your GitHub account.'
)
setToken(null)
} else {
console.log('✅ Successfully retrieved user-specific GitHub token')
setToken(userToken)
}
} catch (err) {
console.error('❌ Error getting GitHub token:', err)
setError(
err instanceof Error ? err.message : 'Failed to get GitHub token'
)
setToken(null)
} finally {
setIsLoading(false)
}
}
useEffect(() => {
if (isAuthLoaded) {
fetchToken()
}
}, [isAuthLoaded, userId])
const refreshToken = async () => {
await fetchToken()
}
return { token, isLoading, error, refreshToken }
}
+124
View File
@@ -0,0 +1,124 @@
// src/hooks/usePayment.ts
import { useState, useCallback } from 'react'
import { useWallet } from '@/context/WalletContext'
import { useGQLClient } from '@/context'
import { paymentService } from '@/services/paymentService'
import { toast } from 'sonner'
interface PaymentConfig {
deploymentType: 'lrn' | 'auction'
selectedDeployer?: { minimumPayment?: string }
maxPrice?: string
deployerCount?: string
}
interface PaymentResult {
txHash: string
success: boolean
}
export function usePayment() {
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [isModalVisible, setIsModalVisible] = useState(false)
const { wallet } = useWallet()
const gqlClient = useGQLClient()
const processPayment = useCallback(async (
config: PaymentConfig
): Promise<PaymentResult | null> => {
if (!wallet?.address) {
throw new Error('Wallet not connected')
}
setIsLoading(true)
setError(null)
try {
// Calculate payment amount
const amount = paymentService.calculatePaymentAmount(config)
// Check if payment is actually required
if (config.deploymentType === 'lrn' && (!config.selectedDeployer?.minimumPayment || amount === '0')) {
// No payment required for this LRN deployer
console.log('No payment required for this deployer')
return {
txHash: '', // Empty string for no payment
success: true
}
}
console.log('Processing payment:', {
amount,
from: wallet.address,
deploymentType: config.deploymentType
})
// Get backend wallet address
const recipientAddress = await gqlClient.getAddress()
console.log('Backend recipient address:', recipientAddress)
toast.info('Requesting payment approval from wallet...')
// Get chainId from environment
const chainId = process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || 'laconic-mainnet'
// Send payment via wallet iframe
const paymentResult = await paymentService.sendPayment({
amount,
senderAddress: wallet.address,
recipientAddress,
chainId
}, setIsModalVisible)
if (!paymentResult.success || !paymentResult.txHash) {
throw new Error(paymentResult.error || 'Payment failed')
}
console.log('Payment successful, txHash:', paymentResult.txHash)
toast.info('Verifying transaction...')
// Verify transaction via backend
const isVerified = await gqlClient.verifyTx(
paymentResult.txHash,
`${amount}alnt`,
wallet.address
)
if (!isVerified) {
throw new Error('Transaction verification failed')
}
toast.success('Payment verified successfully!')
return {
txHash: paymentResult.txHash,
success: true
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Payment failed'
console.error('Payment error:', err)
setError(errorMessage)
toast.error(errorMessage)
return null
} finally {
setIsLoading(false)
setIsModalVisible(false)
}
}, [wallet, gqlClient])
const calculateAmount = useCallback((config: PaymentConfig): string => {
return paymentService.calculatePaymentAmount(config)
}, [])
return {
processPayment,
calculateAmount,
isLoading,
isModalVisible,
setIsModalVisible,
error
}
}
+89 -100
View File
@@ -1,138 +1,127 @@
"use client";
'use client'
import { useState, useEffect } from "react";
import { useAuth, useUser } from "@clerk/nextjs";
import { Octokit } from "@octokit/rest";
import { useOctokit } from '@/context/OctokitContext'
import { useEffect, useState } from 'react'
// Define the return type of the hook
interface UseRepoDataReturn {
repoData: any;
isLoading: boolean;
error: string | null;
repoData: any
isLoading: boolean
error: string | null
}
/**
* A hook to fetch repository data from GitHub
*
*
* @param repoId - The GitHub repository ID to fetch, or empty string to fetch all repos
* @returns Object containing repository data, loading state, and any errors
*/
export function useRepoData(repoId: string): UseRepoDataReturn {
const [repoData, setRepoData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [octokit, setOctokit] = useState<Octokit | null>(null);
// Get auth data from Clerk
const { isLoaded: isAuthLoaded } = useAuth();
const { isLoaded: isUserLoaded, user } = useUser();
const [repoData, setRepoData] = useState<any>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Initialize Octokit with the appropriate token
useEffect(() => {
async function initializeOctokit() {
let token = null;
// Try to get GitHub token from Clerk
if (user) {
try {
// Check if user has connected GitHub account
const githubAccount = user.externalAccounts.find(
account => account.provider === 'github'
);
if (githubAccount) {
// Try to get GitHub OAuth token from Clerk
try {
// token = await user.getToken({ template: 'github' });
console.log('Using GitHub token from Clerk');
} catch (err) {
console.error('Error getting GitHub token from Clerk:', err);
}
}
} catch (err) {
console.error('Error accessing Clerk user data:', err);
}
}
// Fallback to token from environment variable
if (!token && typeof process !== 'undefined') {
token = process.env.NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN || '';
if (token) {
console.warn('Using fallback GitHub token. This should only be used for development.');
}
}
// Create Octokit instance with whatever token we found
if (token) {
setOctokit(new Octokit({ auth: token }));
} else {
setError("No GitHub token available");
setIsLoading(false);
}
}
if (isAuthLoaded && isUserLoaded) {
initializeOctokit();
}
}, [isAuthLoaded, isUserLoaded, user]);
// Use the centralized Octokit context instead of creating our own
const { octokit, isAuth } = useOctokit()
// Fetch repo data when Octokit is available
// Debug the context state
useEffect(() => {
let isMounted = true;
console.log('🔍 useRepoData: Context state changed:', {
hasOctokit: !!octokit,
isAuth,
repoId
})
}, [octokit, isAuth, repoId])
// Fetch repo data when Octokit is available and authenticated
useEffect(() => {
let isMounted = true
async function fetchRepoData() {
if (!octokit) {
return;
// Don't attempt to fetch if not authenticated
if (!isAuth) {
console.log('🔍 fetchRepoData: Not authenticated, skipping fetch')
if (isMounted) {
setError('GitHub authentication required')
setRepoData(null)
setIsLoading(false)
}
return
}
if (!octokit) {
console.log('🔍 fetchRepoData: No octokit instance available')
if (isMounted) {
setError('GitHub client not available')
setRepoData(null)
setIsLoading(false)
}
return
}
console.log('🔍 fetchRepoData: Starting to fetch repos...')
console.log('🔍 fetchRepoData: isAuth =', isAuth)
try {
// Fetch repos from GitHub
const { data: repos } = await octokit.repos.listForAuthenticatedUser();
console.log('🔍 Making GitHub API call: octokit.rest.repos.listForAuthenticatedUser()')
const { data: repos } = await octokit.rest.repos.listForAuthenticatedUser()
console.log('🔍 GitHub API success! Received', repos.length, 'repositories')
// If no repoId is provided, return all repos
if (!repoId) {
if (isMounted) {
setRepoData(repos);
setError(null);
setIsLoading(false);
setRepoData(repos)
setError(null)
setIsLoading(false)
}
return;
return
}
// Find the specific repo by ID if repoId is provided
const repo = repos.find(repo => repo.id.toString() === repoId);
const repo = repos.find((repo: any) => repo.id.toString() === repoId)
if (!repo) {
if (isMounted) {
setError("Repository not found");
setRepoData(null);
setIsLoading(false);
setError('Repository not found')
setRepoData(null)
setIsLoading(false)
}
} else {
if (isMounted) {
setRepoData(repo);
setError(null);
setIsLoading(false);
setRepoData(repo)
setError(null)
setIsLoading(false)
}
}
} catch (err) {
console.error('Error fetching GitHub repo:', err);
console.error('❌ Error fetching GitHub repo:', err)
console.error('❌ Error details:', {
message: err instanceof Error ? err.message : 'Unknown error',
status: (err as any)?.status,
response: (err as any)?.response?.data,
})
if (isMounted) {
setError('Failed to fetch repository data');
setRepoData(null);
setIsLoading(false);
setError('Failed to fetch repository data')
setRepoData(null)
setIsLoading(false)
}
}
}
if (octokit) {
fetchRepoData();
}
return () => {
isMounted = false;
};
}, [repoId, octokit]);
return { repoData, isLoading, error };
}
if (octokit && isAuth) {
fetchRepoData()
} else if (!isAuth) {
// Handle case where we're not authenticated yet
console.log('🔍 useRepoData: Waiting for authentication...')
setIsLoading(true)
setError(null)
}
return () => {
isMounted = false
}
}, [repoId, octokit, isAuth])
return { repoData, isLoading, error }
}
@@ -0,0 +1,273 @@
// src/services/paymentService.ts
interface PaymentConfig {
amount: string
senderAddress: string
recipientAddress: string
chainId: string
}
interface PaymentResult {
txHash: string | null
success: boolean
error?: string
}
class PaymentService {
private readonly WALLET_IFRAME_URL = (process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000') + '/wallet-embed'
private readonly TX_TIMEOUT_MS = 300000
/**
* Send payment via wallet iframe
* This should trigger a modal with the transaction approval iframe
*/
async sendPayment(
config: PaymentConfig,
setModalVisible: (visible: boolean) => void
): Promise<PaymentResult> {
const { amount, senderAddress, recipientAddress, chainId } = config
try {
// Show the transaction approval modal first
setModalVisible(true)
// Wait for the modal iframe to be ready
await new Promise(resolve => setTimeout(resolve, 10000))
// Find the transaction approval iframe (from the modal)
let iframe = document.getElementById('transaction-approval-iframe') as HTMLIFrameElement
if (!iframe) {
// Fallback to other iframe IDs
iframe = document.getElementById('wallet-communication-iframe') as HTMLIFrameElement
}
if (!iframe) {
iframe = document.getElementById('walletIframe') as HTMLIFrameElement
}
console.log('🔵 Looking for wallet iframe:', {
iframeFound: !!iframe,
iframeId: iframe?.id,
hasContentWindow: !!iframe?.contentWindow,
iframeSrc: iframe?.src,
expectedUrl: this.WALLET_IFRAME_URL,
iframeReady: iframe?.contentDocument?.readyState
})
if (!iframe?.contentWindow) {
throw new Error('Wallet iframe not found. Please ensure wallet is connected.')
}
// First, request wallet accounts for this chain to get the correct address
const cosmosAddress = await this.getCosmosAddress(iframe, chainId)
console.log('🔵 Got Cosmos address for chain:', cosmosAddress)
const txHash = await this.requestTransaction({
iframe,
sender: cosmosAddress || senderAddress, // Use cosmos address if available
recipient: recipientAddress,
amount,
chainId,
setModalVisible
})
return {
txHash,
success: true
}
} catch (error) {
// Hide modal on error
setModalVisible(false)
return {
txHash: null,
success: false,
error: error instanceof Error ? error.message : 'Payment failed'
}
}
}
/**
* Get Cosmos address for the specific chain
*/
private async getCosmosAddress(iframe: HTMLIFrameElement, chainId: string): Promise<string | null> {
return new Promise((resolve) => {
const timeoutId = setTimeout(() => {
window.removeEventListener('message', handleMessage)
resolve(null) // Return null if timeout, don't fail the transaction
}, 5000) // Short timeout for address lookup
const handleMessage = (event: MessageEvent) => {
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
window.removeEventListener('message', handleMessage)
clearTimeout(timeoutId)
const accounts = event.data.data
console.log('🔵 Received wallet accounts:', accounts)
// Return the first account address for this chain
if (accounts && accounts.length > 0) {
resolve(accounts[0])
} else {
resolve(null)
}
}
}
window.addEventListener('message', handleMessage)
// Request wallet accounts for this chain
iframe.contentWindow!.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: chainId
},
this.WALLET_IFRAME_URL.replace('/wallet-embed', '') // Use base URL for this request
)
})
}
/**
* Request transaction via iframe communication
*/
private async requestTransaction(params: {
iframe: HTMLIFrameElement
sender: string
recipient: string
amount: string
chainId: string
setModalVisible: (visible: boolean) => void
}): Promise<string | null> {
const { iframe, sender, recipient, amount, chainId, setModalVisible } = params
console.log('🔵 Sending payment request to wallet iframe:', {
type: 'REQUEST_TX',
chainId,
fromAddress: sender,
toAddress: recipient,
amount,
iframeUrl: this.WALLET_IFRAME_URL
})
// Send transaction request to iframe
console.log('🔵 About to postMessage with target origin:', this.WALLET_IFRAME_URL)
iframe.contentWindow!.postMessage(
{
type: 'REQUEST_TX',
chainId: chainId,
fromAddress: sender,
toAddress: recipient,
amount: amount
},
this.WALLET_IFRAME_URL
)
// Also try with '*' to see if it's an origin issue
setTimeout(() => {
console.log('🔵 Sending with wildcard origin as fallback...')
iframe.contentWindow!.postMessage(
{
type: 'REQUEST_TX',
chainId: chainId,
fromAddress: sender,
toAddress: recipient,
amount: amount
},
'*'
)
}, 2000)
console.log('🔵 Payment request sent, waiting for response...')
// Test if iframe can receive messages at all
setTimeout(() => {
console.log('🔵 Testing iframe communication...')
iframe.contentWindow!.postMessage({ type: 'PING' }, this.WALLET_IFRAME_URL)
}, 1000)
// Wait for response
return new Promise((resolve, reject) => {
let timeoutId: NodeJS.Timeout
const handleMessage = (event: MessageEvent) => {
// Log ALL messages for debugging
console.log('🔵 Received message from wallet:', {
origin: event.origin,
type: event.data?.type,
data: event.data
})
// Accept messages from the wallet iframe origin
const allowedOrigins = [
'http://localhost:4000',
'http://localhost:4000/',
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000'
].filter(Boolean)
if (!allowedOrigins.includes(event.origin)) {
console.warn('❌ Message from unexpected origin:', event.origin, 'Allowed:', allowedOrigins)
return
}
console.log('✅ Message from wallet accepted:', event.origin)
console.log('✅ Payment response received:', event.data.type)
if (event.data.type === 'TRANSACTION_RESPONSE') {
cleanup()
setModalVisible(false)
const txHash = event.data.data
console.log('Transaction response:', txHash)
if (!txHash) {
console.log('Transaction was cancelled or failed')
reject(new Error('Transaction was cancelled or failed'))
} else {
console.log('Transaction successful:', txHash)
resolve(txHash)
}
} else if (event.data.type === 'ERROR') {
cleanup()
setModalVisible(false)
console.error('Transaction error:', event.data.message)
reject(new Error(event.data.message || 'Transaction failed'))
}
}
const cleanup = () => {
window.removeEventListener('message', handleMessage)
clearTimeout(timeoutId)
}
// Set up listeners
window.addEventListener('message', handleMessage)
// Set timeout
timeoutId = setTimeout(() => {
cleanup()
setModalVisible(false)
reject(new Error('Transaction timeout - no response from wallet'))
}, this.TX_TIMEOUT_MS)
})
}
/**
* Calculate payment amount based on deployment configuration
*/
calculatePaymentAmount(config: {
deploymentType: 'lrn' | 'auction'
selectedDeployer?: { minimumPayment?: string }
maxPrice?: string
deployerCount?: string
}): string {
const { deploymentType, selectedDeployer, maxPrice, deployerCount } = config
if (deploymentType === 'lrn') {
return selectedDeployer?.minimumPayment?.replace(/\D/g, '') ?? '0'
} else {
if (!maxPrice || !deployerCount) return '0'
return (parseInt(maxPrice) * parseInt(deployerCount)).toString()
}
}
}
export const paymentService = new PaymentService()
+2 -2
View File
@@ -44,10 +44,10 @@ Run these steps in the `apps/deployer/` directory:
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
```
NOTE: Replace `your_clerk_key`, `your_clerk_secret` and `your_github_token` with actual values
NOTE: Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
- Run script to deploy app to `deploy-staging.laconic.co`
+7 -6
View File
@@ -6,10 +6,10 @@ echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
echo "Using AUTHORITY: $AUTHORITY"
# Repository URL
REPO_URL="https://git.vdb.to/deep-stack/laconic-deployer-frontend"
REPO_URL="https://git.vdb.to/NasSharaf/laconic-deployer-frontend"
# Get the latest commit hash for a branch
BRANCH_NAME="ng-deploy-with-fixes"
BRANCH_NAME="main"
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
echo "Latest commit hash for branch $BRANCH_NAME: $LATEST_HASH"
@@ -117,6 +117,7 @@ else
echo "Payment amount is null; skipping payment."
fi
# TODO: Update dns when transitioning from staging to production deployment (deploy.laconic.com)
# Generate application-deployment-request.yml
cat >./records/application-deployment-request.yml <<EOF
record:
@@ -125,15 +126,15 @@ record:
name: laconic-deployer-frontend@$PACKAGE_VERSION
application: lrn://$AUTHORITY/applications/laconic-deployer-frontend@$PACKAGE_VERSION
deployer: $DEPLOYER_LRN
dns: deploy-staging.apps.vaasl.io
dns: deploy-staging.laconic.com
config:
env:
NEXT_PUBLIC_WALLET_IFRAME_URL: https://wallet.laconic.com
NEXT_PUBLIC_LACONICD_CHAIN_ID: laconic-mainnet
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: pk_test_d2VsbC1tYW5hdGVlLTQxLmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY: sk_test_KjVFQ8zzVN7GMtxIgVWTne2YN0FD63gvB5hsfpLiRZ
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23liaet4yc0KX0iM1c
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
CLERK_SECRET_KEY:
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
meta:
note: Added @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
+216 -32
View File
@@ -6,13 +6,13 @@ Ensure you have the following installed:
- [Node.js](https://nodejs.org/) (v18+)
- [pnpm](https://pnpm.io/) (v8+)
- [Git](https://git-scm.com/)
- [ngrok](https://ngrok.com/docs/getting-started/#2-install-the-ngrok-agent-cli)
## Project Structure
This monorepo contains several packages:
- `apps/deploy-fe`: Frontend Next.js application
- `apps/backend`: Express.js backend API
- `apps/deployer`: Deployment service
- `services/gql-client`: GraphQL client library
- `services/ui`: Shared UI components
@@ -20,48 +20,232 @@ This monorepo contains several packages:
## Getting Started
### 1. Clone the repository
The following steps are for running the deploy app locally (using [laconicd mainnet chain](https://laconicd-mainnet-1.laconic.com/status)):
```bash
git clone https://github.com/yourusername/qwrk-laconic-core-develop.git
cd qwrk-laconic-core-develop
```
- Clone laconic-wallet-web repo
```bash
git clone https://git.vdb.to/LaconicNetwork/laconic-wallet-web.git
cd laconic-wallet-web
```
### 2. Install dependencies
- Create .env
```bash
cp .env.example .env
```
```bash
pnpm install
```
- Update values in `.env`
```
# Not required since WalletConnect is not used in iframe integration
REACT_APP_WALLET_CONNECT_PROJECT_ID=
### 3. Configure environment variables
REACT_APP_DEFAULT_GAS_PRICE=0.025
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
REACT_APP_GAS_ADJUSTMENT=2
REACT_APP_LACONICD_RPC_URL=https://laconicd-mainnet-1.laconic.com
Create a `.env.local` file in the `apps/deploy-fe` directory:
# URL of Deploy app frontend app that will run locally
REACT_APP_ALLOWED_URLS=http://localhost:3000
```
```
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
CLERK_SECRET_KEY=your_clerk_secret
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=your_github_token
```
- Install dependencies
```bash
yarn install
```
Create a `local.toml` file in `apps/backend/environments/` (based on the example file).
- Build app
```bash
yarn build
```
### 4. Start the development servers
- Set env values in build
```bash
yarn set-env
```
#### Frontend:
```bash
cd apps/deploy-fe
pnpm dev
```
- Serve the build
```
# Install package serve globally
npm install -g serve
### 5. Connect Your Wallet
# Serve the wallet build
serve -s -l 4000 ./build
```
The application requires a Laconic wallet for certain operations. You need to set up:
Wallet will run at http://localhost:4000
- In a new terminal, clone repo for backend
```bash
git clone https://git.vdb.to/cerc-io/snowballtools-base.git
cd snowballtools-base
```
- Install deps and build
```bash
yarn && yarn build --ignore frontend
```
- Create `packages/backend/environments/local.toml`
```bash
cp packages/backend/environments/local.toml.example packages/backend/environments/local.toml
```
- Update values in `packages/backend/environments/local.toml`
```toml
[server]
host = "127.0.0.1"
port = 8000
gqlPath = "/graphql"
[server.session]
# Can be set to any random string
secret = "RpwqcvFkLZ"
# Frontend webapp URL origin
appOriginUrl = "http://localhost:3000"
# Set to true if server running behind proxy
trustProxy = false
# Backend URL hostname
domain = "localhost"
[database]
dbPath = "db/snowball"
[gitHub]
webhookUrl = ""
[gitHub.oAuth]
clientId = "Ov23lihCWQvOORNxtebD"
clientSecret = "139de35bdd610628289adcd99bb2c7c962a11c99"
[registryConfig]
fetchDeploymentRecordDelay = 5000
checkAuctionStatusDelay = 5000
restEndpoint = "https://laconicd-mainnet-1.laconic.com"
gqlEndpoint = "https://laconicd-mainnet-1.laconic.com/api"
chainId = "laconic-mainnet"
# Set private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
# This account owns the bond and authority configured below
privateKey = ""
bondId = "230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38"
authority = "laconic"
[registryConfig.fee]
gasPrice = "0.001alnt"
[auction]
commitFee = "1000"
commitsDuration = "60s"
revealFee = "1000"
revealsDuration = "60s"
denom = "alnt"
```
- Run ngrok
```bash
ngrok http 8000
```
- Set ngrok URL to `gitHub.webhookUrl` in config `packages/backend/environments/local.toml`
- Example
```toml
...
[gitHub]
webhookUrl = "https://<ngrok-url>.ngrok-free.app"
...
```
- Run backend server
```bash
cd packages/backend
yarn start
```
- In a new terminal, clone repo for frontend (laconic-deployer-frontend)
```bash
git clone https://git.vdb.to/NasSharaf/laconic-deployer-frontend.git
cd laconic-deployer-frontend
```
- Install dependencies
```bash
pnpm install
```
- Build dependecies for frontend app
```bash
pnpm build:fe-compile
```
- Create a `.env.local` file in the `apps/deploy-fe` directory to configure environment variables:
```
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
CLERK_SECRET_KEY=your_clerk_secret
NEXT_PUBLIC_WALLET_IFRAME_URL=http://localhost:4000
NEXT_PUBLIC_LACONICD_CHAIN_ID=laconic-mainnet
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID=Ov23lihCWQvOORNxtebD
```
Replace `your_clerk_key` and `your_clerk_secret` with actual values (<https://clerk.com/docs/quickstarts/nextjs-pages-router#set-your-clerk-api-keys>)
- Start the development server
```bash
cd apps/deploy-fe
pnpm dev
```
- The deploy frontend app will be now available at <http://localhost:3000>
### Fund wallet account
- Change directory to deployer package
```bash
cd apps/deployer/
```
- Setup config for laconic-registry-cli in `packages/deployer/config.yml`
```yaml
services:
registry:
rpcEndpoint: https://laconicd-mainnet-1.laconic.com
gqlEndpoint: https://laconicd-mainnet-1.laconic.com/api
# Set to private key of an account with funds
# Private key of account laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef set in deployer backend can be used
userKey:
chainId: laconic-testnet-2
gasPrice: 0.001alnt
```
- Get wallet account address at <http://localhost:4000> for `laconicd mainnet` network
- Run command to send tokens
```bash
pnpm laconic registry tokens send --address <ACCOUNT_ADDRESS_FROM_WALLET> --type alnt --quantity 12960
```
- To check balance visit <https://explorer.laconic.com/laconic-mainnet/account/ACCOUNT_ADDRESS_FROM_WALLET>
### Misc
- To check for deployments in vaasl : <https://webapp-deployer-ui.apps.vaasl.io/>
- If deployment fails due to low bond balance
- Change directory to deployer package
```bash
cd apps/deployer/
```
- Check balances
```bash
# Account balance
yarn laconic registry account get --address laconic13maulvmjxnyx3g855vk0lsv5aptf3rpxskynef
# Bond balance
yarn laconic registry bond get --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38
```
- Command to refill bond
```bash
yarn laconic registry bond refill --id 230cfedda15e78edc8986dfcb870e1b618f65c56e38d2735476d2a8cb3f25e38 --type alnt --quantity 10000000
```
1. A running wallet instance (follow the setup in the laconic-wallet-web repository)
2. Configure the `NEXT_PUBLIC_WALLET_IFRAME_URL` to point to your wallet instance
## Architecture
@@ -96,7 +280,7 @@ Make sure you have:
## Test deployment
- Follow the dev install steps for stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator/src/branch/main/docs/CONTRIBUTING.md#install>
- Install stack-orchestrator from <https://git.vdb.to/cerc-io/stack-orchestrator#install>
- Build the container for app