593 lines
15 KiB
Markdown
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
|