18 changed files with 1235 additions and 334 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>
)
}
}
@@ -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'
@@ -53,19 +53,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 +82,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 () => {
@@ -231,12 +214,12 @@ export function ConfigureStep() {
// Save configuration to form data
setFormData({
deploymentType: deployOption,
deployerCount: numberOfDeployers,
maxPrice: maxPrice,
selectedLrn: selectedLrn,
organizationSlug: selectedOrg,
selectedOrg: selectedOrg,
paymentAddress: wallet?.address,
selectedLrn: selectedLrn,
deploymentType: deployOption,
maxPrice: maxPrice,
deployerCount: numberOfDeployers,
environmentVariables: validEnvVars
})
@@ -251,11 +234,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">
@@ -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,17 @@ 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 +102,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 +120,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 +137,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 +202,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
})
@@ -18,6 +18,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,11 +27,34 @@ 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']
/** 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
* Used across all onboarding components to maintain flow state
@@ -42,14 +66,7 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']
*/
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: 'connect',
formData: {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: ''
},
formData: initialFormData,
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) =>
set((state) => ({
@@ -66,5 +83,6 @@ export const useOnboarding = create<OnboardingState>((set) => ({
const currentIndex = STEP_ORDER.indexOf(state.currentStep)
const previousStep = STEP_ORDER[currentIndex - 1]
return previousStep ? { currentStep: previousStep } : state
})
}),
resetOnboarding: () => set({ currentStep: 'connect', formData: initialFormData })
}))
@@ -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[]
}
+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 }
}
+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 }
}
+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`
+2 -1
View File
@@ -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:
@@ -133,7 +134,7 @@ record:
NEXT_PUBLIC_API_URL: https://deploy-backend.apps.vaasl.io
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
CLERK_SECRET_KEY:
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN: your_github_token
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID: Ov23li1hxnCcEggrbwED
meta:
note: Added @ $CURRENT_DATE_TIME
repository: "$REPO_URL"
+1 -1
View File
@@ -8,7 +8,7 @@ CLERK_SECRET_KEY=CERC_RUNTIME_ENV_CLERK_SECRET_KEY
NEXT_PUBLIC_WALLET_IFRAME_URL=CERC_RUNTIME_ENV_NEXT_PUBLIC_WALLET_IFRAME_URL
NEXT_PUBLIC_LACONICD_CHAIN_ID=CERC_RUNTIME_ENV_NEXT_PUBLIC_LACONICD_CHAIN_ID
NEXT_PUBLIC_API_URL=CERC_RUNTIME_ENV_NEXT_PUBLIC_API_URL
NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN=CERC_RUNTIME_ENV_NEXT_PUBLIC_GITHUB_FALLBACK_TOKEN
NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID=CERC_RUNTIME_ENV_NEXT_PUBLIC_GITHUB_BACKEND_CLIENT_ID
EOF
pnpm install || exit 1
+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