255 lines
7.3 KiB
TypeScript
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>
|
|
)
|
|
} |