# Phase 3: Clerk Authentication Integration ## Overview Phase 3 completes the wallet integration by connecting it to Clerk authentication, implementing middleware protection, and creating a unified authentication flow. This phase eliminates the need for separate auth systems and provides a seamless experience. ## Timeline **Duration**: 2 weeks **Dependencies**: Phase 1 & 2 completion **Team**: Auth & Backend team ## Architecture ```mermaid graph TD A[Browser] --> B[Next.js Middleware] B --> C{Auth Check} C -->|No Auth| D[Clerk Sign-in] C -->|Has Auth| E[Protected Routes] D --> F[GitHub OAuth] D --> G[Wallet Auth] F --> H[Clerk Session] G --> H H --> I[User Metadata] I --> J[Wallet Address] E --> K[Wallet Operations] K --> L[API Routes] L --> M[Server Actions] ``` ## Migration Reference These implementations adapt functionality from: - Wallet auth flow → from `/repos/laconic-wallet-web/src/screens/AutoSignIn.tsx` - Session management → from `/repos/laconic-wallet-web/src/App.tsx` session handling - Clerk integration → extends existing `/apps/deploy-fe/src/middleware.ts` ## Security Considerations This phase deals with authentication and sensitive wallet operations. Consider these critical security aspects: 1. **Session Management**: Implement proper session handling using Clerk's secure mechanisms 2. **Wallet-Clerk Linking**: Verify wallet ownership through cryptographic signatures before linking to a user account 3. **Authorization**: Use proper authorization checks for all wallet operations 4. **Error States**: Gracefully handle network failures and blockchain errors 5. **Rate Limiting**: Implement rate limiting for sensitive operations like wallet linking ## Step-by-Step Implementation ### 1. Extend Clerk User Metadata Create a TypeScript interface for the extended user metadata in `apps/deploy-fe/src/types/clerk.d.ts`: ```typescript import { User } from '@clerk/nextjs/server' declare module '@clerk/nextjs/server' { interface User { publicMetadata: { walletAddress?: string walletChainId?: string walletConnected?: boolean } } } ``` ### 2. Implement Clerk API Routes for Wallet Auth Create `apps/deploy-fe/src/app/api/clerk/wallet/link/route.ts`: ```typescript import { auth, clerkClient } from '@clerk/nextjs/server' import { validateSignature } from '@workspace/wallet-core' import { NextResponse } from 'next/server' import { z } from 'zod' // Define request validation schema with Zod const linkWalletSchema = z.object({ address: z.string() .refine(addr => /^(0x[a-fA-F0-9]{40}|laconic[a-zA-Z0-9]{39,59})$/.test(addr), { message: "Invalid wallet address format" }), message: z.string().min(10), signature: z.string().min(1), chainId: z.string().min(1) }) export async function POST(request: Request) { // Rate limiting - in production implement proper rate limiting middleware // This is a placeholder to show where it should be implemented const { userId } = await auth() if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } try { const body = await request.json() // Validate request data const validationResult = linkWalletSchema.safeParse(body) if (!validationResult.success) { return NextResponse.json( { error: 'Invalid request data', details: validationResult.error.format() }, { status: 400 } ) } const { address, message, signature, chainId } = validationResult.data // Ensure the signed message actually contains the correct address // This prevents replay attacks where a valid signature for one address is used for another if (!message.includes(address)) { return NextResponse.json( { error: 'Address in message does not match provided address' }, { status: 400 } ) } // Validate signature using wallet-core const isValid = await validateSignature(message, signature) if (!isValid.success) { return NextResponse.json( { error: 'Invalid signature' }, { status: 400 } ) } // Update Clerk user metadata with wallet info await clerkClient.users.updateUser(userId, { publicMetadata: { walletAddress: address, walletChainId: chainId, walletConnected: true } }) return NextResponse.json({ success: true }) } catch (error) { console.error('Error linking wallet:', error) return NextResponse.json( { error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ) } } ``` Create `apps/deploy-fe/src/app/api/clerk/wallet/unlink/route.ts`: ```typescript import { auth, clerkClient } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' export async function POST(request: Request) { const { userId } = await auth() if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } try { const user = await clerkClient.users.getUser(userId) // Remove wallet information from metadata await clerkClient.users.updateUser(userId, { publicMetadata: { ...user.publicMetadata, walletAddress: null, walletChainId: null, walletConnected: false } }) return NextResponse.json({ success: true }) } catch (error) { console.error('Error unlinking wallet:', error) return NextResponse.json( { error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ) } } ``` ### 3. Enhance Middleware with Wallet Verification Update `apps/deploy-fe/src/middleware.ts`: ```typescript import { clerkMiddleware, createRouteMatcher, getAuth } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' // Define routes that require wallet connection const requiresWalletAuth = createRouteMatcher([ '/wallet(.*)', '/projects(.*)', '/buy-prepaid-service(.*)' ]) // Define public routes that don't require any auth const isPublicRoute = createRouteMatcher([ '/sign-in(.*)', '/sign-up(.*)', '/api/github/webhook' ]) export default clerkMiddleware(async (auth, req) => { const { userId } = auth // Skip auth check for webhook endpoint if (req.nextUrl.pathname === '/api/github/webhook') { return NextResponse.next() } // For public routes, allow access if (isPublicRoute(req)) { return NextResponse.next() } // For all other routes, require authentication if (!userId) { return NextResponse.redirect(new URL('/sign-in', req.url)) } // For wallet-required routes, check wallet connection if (requiresWalletAuth(req)) { const user = auth.user // If wallet not connected, redirect to wallet connection page if (!user?.publicMetadata?.walletConnected) { return NextResponse.redirect(new URL('/wallet/connect', req.url)) } } return NextResponse.next() }) export const config = { matcher: [ // Skip Next.js internals and all static files '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', // Always run for API routes '/(api|trpc)(.*)' ] } ``` ### 4. Create Wallet Connection Page Create `apps/deploy-fe/src/app/(web3-authenticated)/wallet/connect/page.tsx`: ```tsx 'use client' import React, { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { useUser } from '@clerk/nextjs' import { WalletConnectButton, useWalletUI } from '@workspace/ui/wallet' import { Button } from '@workspace/ui/components/button' import { PageWrapper } from '@/components/foundation' import { createSiweMessage, signMessage } from '@workspace/wallet-core' import { toast } from 'sonner' import { ArrowLeft } from 'lucide-react' export default function ConnectWalletPage() { const router = useRouter() const { isConnected, wallet } = useWalletUI() const { user, isLoaded } = useUser() const [isLinking, setIsLinking] = useState(false) // Check if user already has a linked wallet useEffect(() => { if (isLoaded && user?.publicMetadata?.walletConnected) { router.push('/') } }, [isLoaded, user, router]) // Function to link wallet to Clerk user const linkWallet = async () => { if (!wallet?.address) return setIsLinking(true) try { // Create SIWE message const message = await createSiweMessage(wallet.address) // Sign message with wallet const signature = await signMessage({ message, namespace: 'eip155', chainId: '1', accountId: 0 }) // Link wallet to Clerk user const response = await fetch('/api/clerk/wallet/link', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address: wallet.address, message, signature, chainId: '1' }) }) if (response.ok) { toast.success('Wallet connected successfully') router.push('/') } else { const data = await response.json() throw new Error(data.error || 'Failed to link wallet') } } catch (error) { console.error('Error linking wallet:', error) toast.error(error instanceof Error ? error.message : 'Failed to link wallet') } finally { setIsLinking(false) } } return (

Connect Your Wallet

Connect your wallet to access additional features like deployments and blockchain-related functionality.

{!isConnected ? ( ) : (

Wallet connected: {wallet?.address?.slice(0, 6)}...{wallet?.address?.slice(-4)}

)}
) } ``` ### 5. Update User Profile to Show Wallet Information Create `apps/deploy-fe/src/components/user-profile/WalletInfo.tsx`: ```tsx 'use client' import React from 'react' import { useUser } from '@clerk/nextjs' import { Button } from '@workspace/ui/components/button' import { WalletConnectButton } from '@workspace/ui/wallet' import { toast } from 'sonner' export function WalletInfo() { const { user, isLoaded } = useUser() const walletAddress = user?.publicMetadata?.walletAddress as string | undefined const walletConnected = user?.publicMetadata?.walletConnected as boolean const disconnectWallet = async () => { try { const response = await fetch('/api/clerk/wallet/unlink', { method: 'POST' }) if (response.ok) { toast.success('Wallet disconnected') // Reload user to update metadata user?.reload() } else { const data = await response.json() throw new Error(data.error || 'Failed to disconnect wallet') } } catch (error) { console.error('Error disconnecting wallet:', error) toast.error(error instanceof Error ? error.message : 'Failed to disconnect wallet') } } if (!isLoaded) { return
} return (

Wallet

{walletConnected && walletAddress ? (

Connected Address:

{walletAddress.slice(0, 8)}...{walletAddress.slice(-6)}

) : (

No wallet connected to your account

)}
) } ``` ### 6. Create Server Action to Check Wallet Auth Create `apps/deploy-fe/src/app/actions/wallet.ts`: ```typescript 'use server' import { auth, clerkClient } from '@clerk/nextjs/server' import { checkBalance } from '@workspace/wallet-core' export async function checkWalletBalance(chainId: string, amount: string) { const { userId } = await auth() if (!userId) { throw new Error('Unauthorized') } const user = await clerkClient.users.getUser(userId) const walletAddress = user.publicMetadata.walletAddress as string | undefined if (!walletAddress) { throw new Error('No wallet connected') } // Use the wallet-core to check balance return checkBalance(chainId, walletAddress, amount) } export async function getWalletStatus() { const { userId } = await auth() if (!userId) { return { isConnected: false } } const user = await clerkClient.users.getUser(userId) const walletConnected = user.publicMetadata.walletConnected as boolean const walletAddress = user.publicMetadata.walletAddress as string | undefined return { isConnected: !!walletConnected, address: walletAddress } } ``` ### 7. Update Balance Checking Components Replace `apps/deploy-fe/src/components/projects/project/deployments/CheckBalanceWrapper.tsx`: ```tsx 'use client' import React, { useEffect, useState } from 'react' import { useRouter } from 'next/navigation' import { checkWalletBalance } from '@/app/actions/wallet' import { toast } from 'sonner' interface CheckBalanceWrapperProps { children: React.ReactNode requiredAmount: string redirectTo?: string } export function CheckBalanceWrapper({ children, requiredAmount, redirectTo = '/buy-prepaid-service' }: CheckBalanceWrapperProps) { const router = useRouter() const [isChecking, setIsChecking] = useState(true) const [hasSufficientBalance, setHasSufficientBalance] = useState() useEffect(() => { const checkBalance = async () => { try { setIsChecking(true) const chainId = process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || '' const result = await checkWalletBalance(chainId, requiredAmount) setHasSufficientBalance(result.hasEnoughBalance) if (!result.hasEnoughBalance) { toast.error('Insufficient balance for this operation') router.push(redirectTo) } } catch (error) { console.error('Error checking balance:', error) toast.error('Failed to check wallet balance') } finally { setIsChecking(false) } } checkBalance() }, [requiredAmount, redirectTo, router]) if (isChecking) { return
} if (hasSufficientBalance === false) { return null } return <>{children} } ``` ### 8. Create Wallet Status React Context Create `apps/deploy-fe/src/context/WalletStatusContext.tsx`: ```tsx 'use client' import React, { createContext, useContext, useEffect, useState } from 'react' import { useUser } from '@clerk/nextjs' interface WalletStatus { isConnected: boolean address?: string } interface WalletStatusContextType { walletStatus: WalletStatus refreshWalletStatus: () => void isLoading: boolean } const WalletStatusContext = createContext(undefined) export function WalletStatusProvider({ children }: { children: React.ReactNode }) { const { user, isLoaded } = useUser() const [isLoading, setIsLoading] = useState(true) const [walletStatus, setWalletStatus] = useState({ isConnected: false }) const refreshWalletStatus = () => { if (isLoaded && user) { const walletConnected = user.publicMetadata.walletConnected as boolean const walletAddress = user.publicMetadata.walletAddress as string | undefined setWalletStatus({ isConnected: !!walletConnected, address: walletAddress }) setIsLoading(false) } else { setWalletStatus({ isConnected: false }) setIsLoading(!isLoaded) } } useEffect(() => { refreshWalletStatus() }, [isLoaded, user]) return ( {children} ) } export function useWalletStatus() { const context = useContext(WalletStatusContext) if (context === undefined) { throw new Error('useWalletStatus must be used within a WalletStatusProvider') } return context } ``` ### 9. Update Providers Update `apps/deploy-fe/src/components/providers/index.tsx`: ```tsx 'use client' import React, { ReactNode } from 'react' import { ThemeProvider } from 'next-themes' import { WalletProvider } from '../wallet/WalletProvider' import { WalletStatusProvider } from '@/context/WalletStatusContext' export function Providers({ children }: { children: ReactNode }) { return ( {children} ) } ``` ### 10. Add Wallet Status to Application UI Update `apps/deploy-fe/src/components/foundation/top-navigation/TopNavigation.tsx`: ```tsx 'use client' import React from 'react' import { WalletConnectButton } from '@workspace/ui/wallet' import { useWalletStatus } from '@/context/WalletStatusContext' import { Button } from '@workspace/ui/components/button' import Link from 'next/link' import { Wallet } from 'lucide-react' export function TopNavigation() { const { walletStatus, isLoading } = useWalletStatus() return (
) } ``` ## Testing 1. Run the frontend app: ```bash cd apps/deploy-fe pnpm dev ``` 2. Test the integration: - Sign in with Clerk (GitHub OAuth) - Connect and link wallet - Check wallet information in user profile - Test protected routes requiring wallet connection - Test balance checking functionality - Test wallet disconnection ## Fallback Handling To ensure robustness, implement these fallback mechanisms: ```typescript // Example fallback for chain disconnections async function getBalanceWithFallback(address: string, chainId: string) { try { // Try primary RPC endpoint return await checkBalance(chainId, address, "1") } catch (error) { console.error("Primary RPC failed:", error) // Try fallback RPC try { const fallbackRpc = getFallbackRpcForChain(chainId) return await checkBalanceWithCustomRpc(fallbackRpc, address, "1") } catch (fallbackError) { console.error("Fallback RPC failed:", fallbackError) throw new Error("Unable to connect to blockchain nodes") } } } ``` ## Integration Complete At this point, the integration of the Laconic wallet within the Next.js application is complete. The system provides: 1. **Unified Authentication**: GitHub OAuth through Clerk combined with wallet authentication 2. **Secure Middleware**: Route protection based on auth status and wallet connection 3. **Clean Architecture**: No iframe dependencies, using server actions and API routes 4. **Improved UX**: Seamless integration with the existing UI components 5. **Robust Validation**: Type-safe validation with Zod throughout the application 6. **Proper Error Handling**: Comprehensive error states with fallback mechanisms This completes all three phases of migrating the wallet from an iframe-based implementation to a native Next.js integration with Clerk auth. ## Additional Resources - [Clerk Webhook Documentation](https://clerk.com/docs/users/sync-data-to-your-backend) - For keeping external systems in sync - [Next.js Middleware Documentation](https://nextjs.org/docs/app/building-your-application/routing/middleware) - For advanced route protection - [Ethers.js Documentation](https://docs.ethers.org/v6/) - For Ethereum wallet functionality - [CosmJS Documentation](https://cosmos.github.io/cosmjs/) - For Cosmos wallet functionality