laconic-deployer-frontend/apps/deploy-fe/src/context/WalletContextProvider.tsx

255 lines
7.3 KiB
TypeScript

// src/context/WalletProvider.tsx
'use client'
import type React from 'react'
import { type ReactNode, useState, useEffect, useCallback } from 'react'
import { toast } from 'sonner'
import { WalletContext, type WalletContextType } from './WalletContext'
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
// Environment variables
const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000'
const WALLET_IFRAME_URL = process.env.NEXT_PUBLIC_WALLET_IFRAME_URL || 'http://localhost:4000'
const WALLET_IFRAME_ID = 'wallet-communication-iframe'
export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// Core wallet state
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null)
const [isConnected, setIsConnected] = useState(false)
const [hasWalletAddress, setHasWalletAddress] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [lastError, setLastError] = useState<string>()
// Modal state for SIWE authentication
const [showAuthModal, setShowAuthModal] = useState(false)
// Check if we have an active backend session
const checkSession = useCallback(async (): Promise<boolean> => {
try {
const response = await fetch(`${BACKEND_URL}/auth/session`, {
method: 'GET',
credentials: 'include',
})
const sessionExists = response.ok
setIsConnected(sessionExists)
if (sessionExists) {
console.log('✅ Active wallet session found')
} else {
console.log('❌ No active wallet session')
}
return sessionExists
} catch (error) {
console.error('Error checking wallet session:', error)
setLastError('Failed to check session')
setIsConnected(false)
return false
}
}, [])
// Initialize session check on mount
useEffect(() => {
checkSession()
}, [checkSession])
// Handle wallet messages from iframe
useEffect(() => {
const handleWalletMessage = (event: MessageEvent) => {
// Security check
if (event.origin !== WALLET_IFRAME_URL) {
console.warn('⚠️ Message from unexpected origin:', event.origin)
return
}
console.log('📨 Wallet message received:', {
type: event.data.type,
origin: event.origin,
data: event.data.data
})
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
handleWalletAccountsData(event.data.data)
}
}
const handleWalletAccountsData = (data: any) => {
let address: string | undefined
// Handle different data formats from wallet
if (Array.isArray(data)) {
if (typeof data[0] === 'string') {
address = data[0]
} else if (data[0]?.address) {
address = data[0].address
}
}
if (address) {
console.log('✅ Wallet address received:', address)
setWallet({
id: address,
address: address
})
setHasWalletAddress(true)
setLastError(undefined)
// Check if we already have a session for this wallet
checkSession().then(hasSession => {
if (!hasSession) {
// Need SIWE authentication
console.log('🔐 Starting SIWE authentication...')
setShowAuthModal(true)
} else {
toast.success('Wallet connected!')
}
})
} else {
console.error('❌ Could not extract address from wallet data:', data)
setLastError('Invalid wallet data received')
toast.error('Invalid wallet data received')
}
}
window.addEventListener('message', handleWalletMessage)
return () => window.removeEventListener('message', handleWalletMessage)
}, [checkSession])
// Connect to wallet
const connect = useCallback(async () => {
if (isLoading) {
console.log('⏸️ Connection already in progress')
return
}
setIsLoading(true)
setLastError(undefined)
try {
console.log('🔌 Attempting to connect wallet...')
// Find the wallet communication iframe
const iframe = document.getElementById(WALLET_IFRAME_ID) as HTMLIFrameElement
if (!iframe) {
throw new Error('Wallet communication interface not found')
}
if (!iframe.contentWindow) {
throw new Error('Wallet interface not loaded')
}
console.log('📤 Sending wallet connection request...')
iframe.contentWindow.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID || '1'
},
WALLET_IFRAME_URL
)
// Set a timeout for connection attempt
const timeoutId = setTimeout(() => {
if (!hasWalletAddress) {
setLastError('Connection timeout')
toast.error('Wallet connection timeout. Please try again.')
setIsLoading(false)
}
}, 15000) // 15 second timeout
// Clear timeout if we get an address
if (hasWalletAddress) {
clearTimeout(timeoutId)
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to connect wallet'
console.error('❌ Error connecting wallet:', error)
setLastError(errorMessage)
toast.error(errorMessage)
setIsLoading(false)
}
}, [isLoading, hasWalletAddress])
// Update loading state when address is received
useEffect(() => {
if (hasWalletAddress && isLoading) {
setIsLoading(false)
}
}, [hasWalletAddress, isLoading])
// Handle successful SIWE authentication
const handleAuthComplete = useCallback((success: boolean) => {
if (success) {
setIsConnected(true)
toast.success('Wallet authentication complete!')
console.log('✅ SIWE authentication successful')
} else {
console.log('❌ SIWE authentication failed')
setLastError('SIWE authentication failed')
toast.error('Wallet authentication failed')
}
setShowAuthModal(false)
}, [])
// Disconnect wallet
const disconnect = useCallback(() => {
setWallet(null)
setIsConnected(false)
setHasWalletAddress(false)
setShowAuthModal(false)
setLastError(undefined)
// Call backend logout
fetch(`${BACKEND_URL}/auth/logout`, {
method: 'POST',
credentials: 'include'
}).catch(console.error)
toast.info('Wallet disconnected')
}, [])
const contextValue: WalletContextType = {
wallet,
isConnected,
hasWalletAddress,
isLoading,
connect,
disconnect,
checkSession,
lastError
}
return (
<WalletContext.Provider value={contextValue}>
{children}
{/* Always-present hidden iframe for wallet communication */}
<iframe
id={WALLET_IFRAME_ID}
src={WALLET_IFRAME_URL}
style={{
position: 'absolute',
left: '-9999px',
width: '1px',
height: '1px',
opacity: 0,
pointerEvents: 'none'
}}
sandbox="allow-scripts allow-same-origin"
title="Wallet Communication Interface"
/>
{/* SIWE Authentication Modal */}
{showAuthModal && (
<AutoSignInIFrameModal
onAuthComplete={handleAuthComplete}
onClose={() => setShowAuthModal(false)}
/>
)}
</WalletContext.Provider>
)
}