# Phase 2: Wallet UI Components Implementation ## Overview Phase 2 focuses on building UI components for the wallet and integrating them with the Next.js frontend. This phase transforms the core wallet functionality into usable UI components and connects them to the application. ## Timeline **Duration**: 2 weeks **Dependencies**: Phase 1 (wallet-core package) **Team**: Frontend team ## Directory Structure ```mermaid graph TD A[services/ui/src/wallet] --> B[components/] A --> C[hooks/] A --> D[providers/] B --> B1[WalletConnectButton.tsx] B --> B2[WalletModal.tsx] B --> B3[TransactionApproval.tsx] B --> B4[SignMessageModal.tsx] B --> B5[AccountSelector.tsx] B --> B6[NetworkSelector.tsx] B --> B7[BalanceDisplay.tsx] C --> C1[useWalletUI.ts] C --> C2[useTransaction.ts] D --> D1[WalletUIProvider.tsx] E[apps/deploy-fe/src] --> F[components/wallet/] F --> F1[WalletProvider.tsx] F --> F2[ConnectWallet.tsx] G[apps/deploy-fe/src/app/api] --> H[wallet/] H --> H1[balance/route.ts] H --> H2[sign/route.ts] H --> H3[connect/route.ts] ``` ## Migration Reference These implementations adapt functionality from: - `WalletModal.tsx` → from `/repos/laconic-wallet-web/src/components/wallet/AutoSignInIFrameModal.tsx` - `TransactionApproval.tsx` → from `/repos/laconic-wallet-web/src/screens/ApproveTransaction.tsx` - `WalletUIProvider.tsx` → from `/repos/laconic-wallet-web/src/context/WalletContextProvider.tsx` ## Security Considerations Before implementation, please note these important security considerations: 1. **CORS Configuration**: API routes must have proper CORS configuration to prevent unauthorized access 2. **Input Validation**: All user input and transaction data must be validated server-side using Zod or similar 3. **Error Handling**: Implement comprehensive error handling to prevent information leakage 4. **Key Management**: Never expose private keys in client-side code 5. **Signature Verification**: Always verify signatures on the server-side 6. **Responsive Design**: Wallet UI components should be mobile-responsive with proper accessibility attributes ## Step-by-Step Implementation ### 1. Create Wallet UI Components Create the directory structure: ```bash mkdir -p services/ui/src/wallet/components mkdir -p services/ui/src/wallet/hooks mkdir -p services/ui/src/wallet/providers ``` ### 2. Implement Key Components Create `services/ui/src/wallet/components/WalletConnectButton.tsx`: ```tsx 'use client' import { Button } from '@workspace/ui/components/button' import React from 'react' import { useWalletUI } from '../hooks/useWalletUI' interface WalletConnectButtonProps { variant?: 'default' | 'outline' | 'ghost' size?: 'default' | 'sm' | 'lg' } export function WalletConnectButton({ variant = 'default', size = 'default' }: WalletConnectButtonProps) { const { isConnected, connect, disconnect, wallet } = useWalletUI() return ( {isConnected ? `Connected: ${wallet?.address?.slice(0, 6)}...${wallet?.address?.slice(-4)}` : 'Connect Wallet'} ) } ``` Create `services/ui/src/wallet/components/WalletModal.tsx`: ```tsx 'use client' import React, { useEffect } from 'react' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@workspace/ui/components/dialog' import { Button } from '@workspace/ui/components/button' import { useWalletUI } from '../hooks/useWalletUI' import { signIn } from '@workspace/wallet-core' export function WalletModal() { const { isOpen, closeModal, wallet, setWallet, connectWallet } = useWalletUI() useEffect(() => { async function handleSignIn() { if (wallet?.address) { try { // Replace the direct iframe messaging with server action await signIn(wallet.address) } catch (error) { console.error('Error during sign-in:', error) } } } if (wallet?.address) { handleSignIn() } }, [wallet]) return ( Connect your wallet Connect Laconic Wallet ) } ``` Create `services/ui/src/wallet/components/TransactionApproval.tsx`: ```tsx 'use client' import React, { useState } from 'react' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@workspace/ui/components/card' import { Button } from '@workspace/ui/components/button' import { useWalletUI } from '../hooks/useWalletUI' interface TransactionApprovalProps { amount: string recipient: string denom: string onApprove: () => void onReject: () => void } export function TransactionApproval({ amount, recipient, denom, onApprove, onReject }: TransactionApprovalProps) { const { wallet } = useWalletUI() const [isProcessing, setIsProcessing] = useState(false) const handleApprove = async () => { setIsProcessing(true) try { await onApprove() } finally { setIsProcessing(false) } } return ( Approve Transaction Review and approve this transaction From: {wallet?.address || 'Not connected'} To: {recipient} Amount: {amount} {denom} Reject {isProcessing ? 'Processing...' : 'Approve'} ) } ``` ### 3. Implement UI Hooks Create `services/ui/src/wallet/hooks/useWalletUI.ts`: ```tsx 'use client' import { useEffect, useState } from 'react' import { useWallet } from '@workspace/wallet-core' import { toast } from 'sonner' export function useWalletUI() { const walletCore = useWallet() const [isOpen, setIsOpen] = useState(false) const openModal = () => setIsOpen(true) const closeModal = () => setIsOpen(false) const connectWallet = async () => { try { // This now uses server actions instead of iframe messaging await walletCore.connect() closeModal() toast.success('Wallet connected') } catch (error) { toast.error('Failed to connect wallet') console.error(error) } } const disconnect = () => { walletCore.disconnect() toast.info('Wallet disconnected') } // Expose the core wallet methods and UI-specific methods return { ...walletCore, isOpen, openModal, closeModal, connectWallet } } ``` ### 4. Implement Provider Create `services/ui/src/wallet/providers/WalletUIProvider.tsx`: ```tsx 'use client' import React, { createContext, useContext, useEffect, useState } from 'react' import { WalletProvider } from '@workspace/wallet-core' import { WalletModal } from '../components/WalletModal' import { toast } from 'sonner' interface WalletUIContextType { openWalletModal: () => void closeWalletModal: () => void isModalOpen: boolean } const WalletUIContext = createContext({ openWalletModal: () => {}, closeWalletModal: () => {}, isModalOpen: false }) export function useWalletUIContext() { return useContext(WalletUIContext) } export function WalletUIProvider({ children }: { children: React.ReactNode }) { const [isModalOpen, setIsModalOpen] = useState(false) const openWalletModal = () => setIsModalOpen(true) const closeWalletModal = () => setIsModalOpen(false) return ( {children} ) } ``` ### 5. Create Index Exports Create `services/ui/src/wallet/index.ts`: ```typescript // Component exports export { WalletConnectButton } from './components/WalletConnectButton' export { WalletModal } from './components/WalletModal' export { TransactionApproval } from './components/TransactionApproval' export { AccountSelector } from './components/AccountSelector' export { NetworkSelector } from './components/NetworkSelector' export { BalanceDisplay } from './components/BalanceDisplay' // Hook exports export { useWalletUI } from './hooks/useWalletUI' export { useTransaction } from './hooks/useTransaction' // Provider exports export { WalletUIProvider, useWalletUIContext } from './providers/WalletUIProvider' ``` ### 6. Create API Routes for Wallet Communication Create `apps/deploy-fe/src/app/api/wallet/balance/route.ts`: ```typescript import { checkBalance } from '@workspace/wallet-core' import { auth } 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 body = await request.json() const { chainId, address, amount } = body const result = await checkBalance(chainId, address, amount) return NextResponse.json(result) } catch (error) { console.error('Balance check error:', error) return NextResponse.json( { error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ) } } ``` Create `apps/deploy-fe/src/app/api/wallet/sign/route.ts`: ```typescript import { validateSignature } from '@workspace/wallet-core' import { auth } from '@clerk/nextjs/server' import { NextResponse } from 'next/server' import { z } from 'zod' // Define validation schema const signRequestSchema = z.object({ message: z.string().min(1, "Message is required"), signature: z.string().min(1, "Signature is required") }) export async function POST(request: Request) { const { userId } = await auth() if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } try { const body = await request.json() // Validate input data const result = signRequestSchema.safeParse(body) if (!result.success) { return NextResponse.json( { error: 'Invalid request data', details: result.error.format() }, { status: 400 } ) } const { message, signature } = result.data // Add additional validation for signature format if needed // (e.g., check if it matches expected pattern) const validationResult = await validateSignature(message, signature) return NextResponse.json(validationResult) } catch (error) { console.error('Signature validation error:', error) return NextResponse.json( { error: error instanceof Error ? error.message : 'Unknown error' }, { status: 500 } ) } } ``` ### 7. Integrate with Next.js App Create `apps/deploy-fe/src/components/wallet/WalletProvider.tsx`: ```tsx 'use client' import { ReactNode } from 'react' import { WalletUIProvider } from '@workspace/ui/wallet' export function WalletProvider({ children }: { children: ReactNode }) { return {children} } ``` 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' export function Providers({ children }: { children: ReactNode }) { return ( {children} ) } ``` ## Integration Example Create a wallet connection button in `apps/deploy-fe/src/components/foundation/top-navigation/TopNavigation.tsx`: ```tsx 'use client' import React from 'react' import { WalletConnectButton } from '@workspace/ui/wallet' export function TopNavigation() { return ( {/* Logo and other navigation items */} {/* Right side items */} {/* Other right-side items */} ) } ``` Replace `apps/deploy-fe/src/components/iframe/check-balance-iframe/CheckBalanceIframe.tsx` with server action: ```tsx 'use client' import { useEffect } from 'react' import { checkBalance } from '@workspace/wallet-core' interface CheckBalanceProps { onBalanceChange: (value: boolean | undefined) => void isPollingEnabled: boolean amount: string } export default function CheckBalance({ onBalanceChange, isPollingEnabled, amount }: CheckBalanceProps) { useEffect(() => { let interval: NodeJS.Timeout const fetchBalance = async () => { try { // Uses server action instead of iframe const chainId = process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || '' const result = await fetch('/api/wallet/balance', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chainId, amount }) }).then(res => res.json()) onBalanceChange(result.hasEnoughBalance) } catch (error) { console.error('Error checking balance:', error) onBalanceChange(undefined) } } fetchBalance() if (isPollingEnabled) { interval = setInterval(fetchBalance, 5000) } return () => { if (interval) clearInterval(interval) } }, [amount, isPollingEnabled, onBalanceChange]) // This component doesn't render anything visually return null } ``` ## Testing 1. Build the UI package: ```bash cd services/ui pnpm build ``` 2. Run the frontend app: ```bash cd apps/deploy-fe pnpm dev ``` 3. Test wallet functionality: - Connection button in navigation - Login flow with wallet - Balance checks - Transaction signing ## Next Steps - Phase 3: Complete Clerk auth integration - Phase 3: Implement comprehensive middleware - Phase 3: Develop unified authentication flow