laconic-deployer-frontend/docs/architecture/wallet_migration/2-phase-2-wallet-ui.md

593 lines
15 KiB
Markdown

# 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 (
<Button
variant={variant}
size={size}
onClick={isConnected ? disconnect : connect}
>
{isConnected
? `Connected: ${wallet?.address?.slice(0, 6)}...${wallet?.address?.slice(-4)}`
: 'Connect Wallet'}
</Button>
)
}
```
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 (
<Dialog open={isOpen} onOpenChange={closeModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Connect your wallet</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<Button onClick={connectWallet}>
Connect Laconic Wallet
</Button>
</div>
</DialogContent>
</Dialog>
)
}
```
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 (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Approve Transaction</CardTitle>
<CardDescription>Review and approve this transaction</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-3 gap-2">
<div className="text-sm font-medium">From:</div>
<div className="col-span-2 text-sm truncate">
{wallet?.address || 'Not connected'}
</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="text-sm font-medium">To:</div>
<div className="col-span-2 text-sm truncate">{recipient}</div>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="text-sm font-medium">Amount:</div>
<div className="col-span-2 text-sm">
{amount} {denom}
</div>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-between">
<Button
variant="outline"
onClick={onReject}
disabled={isProcessing}
>
Reject
</Button>
<Button
onClick={handleApprove}
disabled={isProcessing}
>
{isProcessing ? 'Processing...' : 'Approve'}
</Button>
</CardFooter>
</Card>
)
}
```
### 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<WalletUIContextType>({
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 (
<WalletProvider>
<WalletUIContext.Provider
value={{
openWalletModal,
closeWalletModal,
isModalOpen
}}
>
{children}
<WalletModal open={isModalOpen} onOpenChange={setIsModalOpen} />
</WalletUIContext.Provider>
</WalletProvider>
)
}
```
### 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 <WalletUIProvider>{children}</WalletUIProvider>
}
```
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 (
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
<WalletProvider>{children}</WalletProvider>
</ThemeProvider>
)
}
```
## 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 (
<header className="border-b sticky top-0 z-40 bg-background">
<div className="container flex h-16 items-center px-4 sm:px-6">
<nav className="flex-1 flex items-center justify-between">
{/* Logo and other navigation items */}
{/* Right side items */}
<div className="flex items-center space-x-4">
<WalletConnectButton />
{/* Other right-side items */}
</div>
</nav>
</div>
</header>
)
}
```
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