/** * UP Connector - Main class that orchestrates avatar and modal * Provides smooth avatar-to-modal animations and connection state management * * NOTE: This connector uses Lit-based modal components: * - for wallet connection (configured via setupConnectModal()) * - for account management (shows address, chain, disconnect) * * Current implementation status: * - ✅ Avatar integration (DraggableAvatar with IconView) * - ✅ Connection modal (Lit-based ConnectModal) * - ✅ Account management modal (Lit-based AccountModal) * - ✅ Wagmi synchronization * * The legacy ConnectorModal (vanilla JS) has been removed. The config.modal property * is deprecated and has no effect - use setupConnectModal() instead. * * Users can pre-place modal components in DOM for custom positioning: * - Place or anywhere in your HTML * - The connector will find and reuse existing modals instead of creating new ones */ import { DraggableAvatar } from './avatar.js' import '@lukso/up-modal/connect-modal' // Import for side-effect (registers custom element) import './account-modal.js' // Import for side-effect (registers custom element) import { setLuksoConfig } from '@lukso/transaction-view-headless' import type { ConnectModal, WalletConnector, } from '@lukso/up-modal/connect-modal' import { disconnect, getConnection, watchConnection, } from '@lukso/up-modal/connect-modal' import type { AccountModal } from './account-modal.js' import type { ConnectionState, ConnectorConfig } from './types.js' const DEFAULT_CONFIG: Required< Omit > = { autoConnect: false, persistConnection: true, storageKey: 'up-connector-state', chainId: 42, // LUKSO mainnet rpcUrl: 'https://rpc.lukso.network', } export class UPConnector { private config: ConnectorConfig & typeof DEFAULT_CONFIG private avatar?: DraggableAvatar private connectModal?: ConnectModal private accountModal?: AccountModal private connectionState: ConnectionState = 'disconnected' private connectedAddress?: string private connectedProvider?: any private connectedChainId?: number private wagmiUnsubscribe?: (() => void) | null connectors: WalletConnector[] = [] constructor(config: ConnectorConfig = {}) { this.config = { ...DEFAULT_CONFIG, ...config } // Initialize components this.init() } private async init(): Promise { // Create avatar immediately await this.createAvatar() // Start watching wagmi (handles both initial state and changes) // This is async but non-blocking - connector works immediately this.startWagmiSync() // Try auto-connect if enabled (for non-wagmi connections) if (this.config.autoConnect && this.config.persistConnection) { await this.tryAutoConnect() } } /** * Start syncing with wagmi * Checks current state and subscribes to changes */ /** * Update global Lukso config for address resolution */ private updateLuksoConfig(chainId: number): void { try { if (chainId === 42) { // LUKSO Mainnet setLuksoConfig({ chainId: 42, rpcUrl: 'https://rpc.lukso.network', }) } else if (chainId === 4201) { // LUKSO Testnet setLuksoConfig({ chainId: 4201, rpcUrl: 'https://rpc.testnet.lukso.network', }) } else { // Other networks - default to mainnet config but with custom chainId setLuksoConfig({ chainId, rpcUrl: this.config.rpcUrl, }) } } catch (error) { console.warn('⚠️ [Connector] Failed to update Lukso config:', error) } } private async startWagmiSync(): Promise { try { // Check initial state first const connection = await getConnection() if (!connection) { return } if (connection.status === 'connected' && connection.address) { this.handleConnection( connection.address, connection.connector, connection.chainId ) } // Subscribe to wagmi connection changes this.wagmiUnsubscribe = await watchConnection((connection) => { const account = { isConnected: connection.status === 'connected', address: connection.address, chainId: connection.chainId, connector: connection.connector, } if (account.isConnected && account.address) { // Connected if ( this.connectedAddress !== account.address || this.connectedChainId !== account.chainId ) { this.handleConnection( account.address, account.connector, account.chainId ) } } else { // Disconnected if (this.connectionState === 'connected') { this.handleDisconnection() } } }) } catch (error) { console.warn('⚠️ [Connector] Failed to sync with wagmi:', error) } } private async createAvatar(): Promise { this.avatar = new DraggableAvatar({ // Default avatar options avatarSize: 'medium', initialPosition: 'top-right', fallbackText: '👤', // Override with user config ...this.config.avatar, // Connection-specific callbacks onClick: (event, data) => { // Call user-defined onClick first this.config.avatar?.onClick?.(event, data) // Then handle connection logic this.handleAvatarClick(event) }, onAddressClick: (event) => { // Call user-defined callback this.config.avatar?.onAddressClick?.(event) }, }) // Update avatar connected state this.updateAvatarState() } // TODO: Replace with Lit-based account management modal // Legacy createModal() method removed - was using deprecated ConnectorModal private handleAvatarClick(_event: MouseEvent): void { if (this.connectionState === 'connected') { // Show account management modal this.showAccountModal() } else { // Show connection modal this.showConnectionModal() } } private showConnectionModal(): void { // First, check if user has placed a connect-modal in the DOM if (!this.connectModal) { this.connectModal = document.querySelector( 'connect-modal' ) as ConnectModal } // If still not found, create and append it if (!this.connectModal) { this.connectModal = document.createElement( 'connect-modal' ) as ConnectModal document.body.appendChild(this.connectModal as unknown as HTMLElement) // Handle connect event this.connectModal.addEventListener('connect', ((e: CustomEvent) => { const _connector = e.detail.connector as WalletConnector // TODO: Handle actual connection logic // For now, this will be extended when wagmi/viem integration is added }) as EventListener) // Handle close event this.connectModal.addEventListener('close', (() => { // Modal closed }) as EventListener) } // Modal will load connectors automatically from global setup // Open the modal this.connectModal.open = true } private showAccountModal(): void { if (!this.connectedAddress) return // First, check if user has placed an account-modal in the DOM if (!this.accountModal) { this.accountModal = document.querySelector( 'account-modal' ) as AccountModal } // If still not found, create and append it if (!this.accountModal) { this.accountModal = document.createElement( 'account-modal' ) as AccountModal document.body.appendChild(this.accountModal) // Handle disconnect event this.accountModal.addEventListener('disconnect', () => { this.handleDisconnection() }) } // Set properties and open this.accountModal.address = this.connectedAddress this.accountModal.chainId = this.connectedChainId this.accountModal.connectorName = this.connectedProvider?.name || 'Unknown' this.accountModal.open = true } private handleConnection( address: string, provider: any, chainId?: number ): void { this.connectedAddress = address this.connectedProvider = provider this.connectedChainId = chainId this.setConnectionState('connected') // Update global Lukso config for address resolution if (chainId) { this.updateLuksoConfig(chainId) } // Update avatar with connected address and chainId if (this.avatar) { this.avatar.updateAddress(address, undefined, chainId) this.avatar.setConnected(true) } // Persist connection if enabled if (this.config.persistConnection) { this.saveConnectionState() } // Hide connection modal if open if (this.connectModal) { this.connectModal.open = false } } private async handleDisconnection(): Promise { // Disconnect from wagmi first to ensure state is clean await disconnect() this.connectedAddress = undefined this.connectedProvider = undefined this.connectedChainId = undefined this.setConnectionState('disconnected') // Update avatar - clear address and show disconnected state if (this.avatar) { this.avatar.setConnected(false) // Clear the address to show fallback/disconnected state this.avatar.updateAddress('') } // Clear persisted state if (this.config.persistConnection) { this.clearConnectionState() } // Hide connect modal if open if (this.connectModal) { this.connectModal.open = false } } private setConnectionState(state: ConnectionState): void { const prevState = this.connectionState this.connectionState = state // Update avatar state this.updateAvatarState() // Notify callback if (prevState !== state) { this.config.onConnectionChange?.(state, { address: this.connectedAddress, provider: this.connectedProvider, chainId: this.connectedChainId, }) } } private updateAvatarState(): void { if (!this.avatar) return this.avatar.setConnected(this.connectionState === 'connected') // Update avatar address and chainId if connected if (this.connectionState === 'connected' && this.connectedAddress) { this.avatar.updateAddress( this.connectedAddress, undefined, this.connectedChainId ) } } private async tryAutoConnect(): Promise { const savedState = this.loadConnectionState() if (!savedState?.address) return this.setConnectionState('connecting') try { // Try to reconnect using saved provider info // This would typically involve checking if the provider is still available // and attempting to reconnect // For now, simulate auto-connection setTimeout(() => { if (savedState.address) { this.handleConnection(savedState.address, savedState.provider) } else { this.setConnectionState('disconnected') } }, 1000) } catch (error) { console.warn('Auto-connect failed:', error) this.setConnectionState('disconnected') } } private saveConnectionState(): void { if (typeof localStorage === 'undefined') return try { const state = { address: this.connectedAddress, provider: this.connectedProvider?.constructor?.name, timestamp: Date.now(), } localStorage.setItem(this.config.storageKey, JSON.stringify(state)) } catch (error) { console.warn('Failed to save connection state:', error) } } private loadConnectionState(): any { if (typeof localStorage === 'undefined') return null try { const saved = localStorage.getItem(this.config.storageKey) if (!saved) return null const state = JSON.parse(saved) // Check if state is not too old (24 hours) const maxAge = 24 * 60 * 60 * 1000 if (Date.now() - state.timestamp > maxAge) { this.clearConnectionState() return null } return state } catch (error) { console.warn('Failed to load connection state:', error) return null } } private clearConnectionState(): void { if (typeof localStorage === 'undefined') return try { localStorage.removeItem(this.config.storageKey) } catch (error) { console.warn('Failed to clear connection state:', error) } } // Public API public getAvatar(): DraggableAvatar | undefined { return this.avatar } /** * @deprecated Legacy modal removed. Use getConnectModal() or create account modal */ public getModal(): undefined { console.warn('getModal() is deprecated - legacy ConnectorModal removed') return undefined } public getConnectModal(): ConnectModal | undefined { return this.connectModal } /** * @deprecated Use setupConnectModal() instead to configure wagmi connectors globally */ public setConnectors(connectors: WalletConnector[]): void { this.connectors = connectors // Note: The ConnectModal now loads connectors automatically from global setup // Use setupConnectModal() to configure wagmi integration console.warn( 'setConnectors() is deprecated. Use setupConnectModal() instead.' ) } public async connect(providerId?: string): Promise { this.setConnectionState('connecting') if (providerId) { // Connect to specific provider // This would be implemented based on available providers } else { // Show connection modal this.showConnectionModal() } } public disconnect(): void { this.handleDisconnection() } public getConnectionState(): ConnectionState { return this.connectionState } public getConnectedAddress(): string | undefined { return this.connectedAddress } public getConnectedProvider(): any { return this.connectedProvider } public isConnected(): boolean { return this.connectionState === 'connected' } public showModal(): void { if (this.isConnected()) { this.showAccountModal() } else { this.showConnectionModal() } } public hideModal(): void { if (this.connectModal) { this.connectModal.open = false } } public updateConfig(config: Partial): void { this.config = { ...this.config, ...config } // Update avatar if config changed if (config.avatar && this.avatar) { // This would require implementing avatar config updates } } public destroy(): void { // Unsubscribe from wagmi changes if (this.wagmiUnsubscribe) { this.wagmiUnsubscribe() this.wagmiUnsubscribe = undefined } this.avatar?.destroy() // Clean up connect modal if (this.connectModal) { this.connectModal.open = false if (this.connectModal.parentNode) { this.connectModal.parentNode.removeChild(this.connectModal) } } this.avatar = undefined this.connectModal = undefined this.connectedAddress = undefined this.connectedProvider = undefined this.connectionState = 'disconnected' } } /** * Factory function for creating UP Connector instances */ export function createConnector(config: ConnectorConfig = {}): UPConnector { return new UPConnector(config) } export default UPConnector