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
@@ -25,24 +26,52 @@ export default function ProjectsPage() {
const [error, setError] = useState<string | null>(null)
const client = useGQLClient()
const { user } = useUser()
const handleCreateProject = () => {
window.location.href = '/projects/github/ps/cr'
}
useEffect(() => {
if (user !== undefined) {
loadAllProjects()
}, [])
}
}, [user])
const loadAllProjects = async () => {
try {
setIsLoading(true)
setError(null)
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) {
if (
!orgsResponse.organizations ||
orgsResponse.organizations.length === 0
) {
setProjects([])
setIsLoading(false)
return
@@ -53,10 +82,24 @@ export default function ProjectsPage() {
for (const org of orgsResponse.organizations) {
try {
const projectsResponse = await client.getProjectsInOrganization(org.slug)
const projectsResponse = await client.getProjectsInOrganization(
org.slug
)
// Transform GraphQL projects to match ProjectData interface
const orgProjects: ProjectData[] = projectsResponse.projectsInOrganization.map((project: Project) => ({
// 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,
@@ -65,13 +108,20 @@ export default function ProjectsPage() {
deployments: project.deployments || []
}))
allProjects.push(...orgProjects)
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)
@@ -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
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,7 +200,9 @@ 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
@@ -172,17 +231,21 @@ export default function ProjectsPage() {
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`
}
}))
}
@@ -208,10 +271,10 @@ 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()
@@ -20,10 +20,14 @@ export function SuccessStep() {
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 = () => {
@@ -62,7 +66,9 @@ export function SuccessStep() {
</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">
@@ -70,7 +76,9 @@ export function SuccessStep() {
</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>
@@ -89,17 +97,49 @@ export function SuccessStep() {
{/* Next steps section */}
<div className="mb-8">
<h3 className={`text-lg font-medium ${isDarkMode ? "text-white" : "text-zinc-900"} mb-4`}>Next steps</h3>
<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={`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>
<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"/>
<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"
/>
</svg>
</Button>
</div>
@@ -114,8 +154,19 @@ export function SuccessStep() {
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"/>
<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>
@@ -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[]
}
+50 -11
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
@@ -95,13 +95,30 @@ export function useAuthStatus(): AuthStatus & AuthActions {
// 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,9 +128,10 @@ 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(() => {
@@ -122,7 +140,7 @@ export function useAuthStatus(): AuthStatus & AuthActions {
} else {
setIsGithubBackendAuth(false)
}
}, [isBackendConnected, checkGithubBackendAuth])
}, [isBackendConnected]) // Remove checkGithubBackendAuth from dependencies to prevent infinite loop
// Check backend connection when wallet session is active (SIWE completed)
useEffect(() => {
@@ -133,11 +151,14 @@ 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'
const hasGithubInClerk =
user?.externalAccounts?.find(
(account) =>
account.provider === 'github' ||
account.verification?.strategy === 'oauth_github'
) !== undefined
// Calculate what's missing
@@ -163,10 +184,28 @@ export function useAuthStatus(): AuthStatus & AuthActions {
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 () => {
@@ -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 }
}
+76 -87
View File
@@ -1,14 +1,13 @@
"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
}
/**
@@ -18,121 +17,111 @@ interface UseRepoDataReturn {
* @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);
const [repoData, setRepoData] = useState<any>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Get auth data from Clerk
const { isLoaded: isAuthLoaded } = useAuth();
const { isLoaded: isUserLoaded, user } = useUser();
// Use the centralized Octokit context instead of creating our own
const { octokit, isAuth } = useOctokit()
// Initialize Octokit with the appropriate token
// Debug the context state
useEffect(() => {
async function initializeOctokit() {
let token = null;
console.log('🔍 useRepoData: Context state changed:', {
hasOctokit: !!octokit,
isAuth,
repoId
})
}, [octokit, isAuth, repoId])
// 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]);
// Fetch repo data when Octokit is available
// Fetch repo data when Octokit is available and authenticated
useEffect(() => {
let isMounted = true;
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();
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]);
isMounted = false
}
}, [repoId, octokit, isAuth])
return { repoData, isLoading, error };
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
+203 -19
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)):
- Clone laconic-wallet-web repo
```bash
git clone https://github.com/yourusername/qwrk-laconic-core-develop.git
cd qwrk-laconic-core-develop
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
```
- Update values in `.env`
```
# Not required since WalletConnect is not used in iframe integration
REACT_APP_WALLET_CONNECT_PROJECT_ID=
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
# URL of Deploy app frontend app that will run locally
REACT_APP_ALLOWED_URLS=http://localhost:3000
```
- Install dependencies
```bash
yarn install
```
- Build app
```bash
yarn build
```
- Set env values in build
```bash
yarn set-env
```
- Serve the build
```
# Install package serve globally
npm install -g serve
# Serve the wallet build
serve -s -l 4000 ./build
```
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
```
### 3. Configure environment variables
Create a `.env.local` file in the `apps/deploy-fe` directory:
- 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_FALLBACK_TOKEN=your_github_token
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>)
Create a `local.toml` file in `apps/backend/environments/` (based on the example file).
### 4. Start the development servers
#### Frontend:
- Start the development server
```bash
cd apps/deploy-fe
pnpm dev
```
### 5. Connect Your Wallet
- The deploy frontend app will be now available at <http://localhost:3000>
The application requires a Laconic wallet for certain operations. You need to set up:
### 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