/** * Restore Modal - Lit Component * * Framework-agnostic restore modal for wallet data import. * Uses lukso-modal from @lukso/web-components for consistent UI. */ import { safeCustomElement } from '@lukso/core/utils' import type { BackupFile, PasskeyAuthProvider, WalletData, } from '@lukso/passkey-auth' import { readBackupFile } from '@lukso/passkey-auth' import { html, nothing } from 'lit' import { property, state } from 'lit/decorators.js' import { CoreLitElement } from './styles' // Import lukso web components import '@lukso/web-components/dist/components/lukso-modal' import '@lukso/web-components/dist/components/lukso-button' import '@lukso/web-components/dist/components/lukso-input' enum Step { UPLOAD = 1, PASSWORD = 2, PREVIEW = 3, SUCCESS = 4, } @safeCustomElement('restore-modal') export class RestoreModal extends CoreLitElement { // Public properties @property({ type: Boolean, reflect: true }) isOpen = false @property({ type: String }) theme: 'light' | 'dark' | 'auto' = 'auto' @property({ type: Object }) keyGenerator: PasskeyAuthProvider | null = null // Private state @state() private currentStep: Step = Step.UPLOAD @state() private selectedFile: File | null = null @state() private backupData: BackupFile | null = null @state() private fileError: string | null = null @state() private isDragging = false @state() private password = '' @state() private passwordErrors: string[] = [] @state() private isRestoring = false @state() private validationWarnings: string[] = [] @state() private controllers: Array<{ address: string status: 'new' | 'existing' }> = [] @state() private restoredAddresses: string[] = [] validationInfo: string[] = [] walletData: WalletData | null = null /** * Public method for parent to set preview result * Called after parent handles the 'preview' event */ public setPreviewResult(options: { controllers: Array<{ address: string; status: 'new' | 'existing' }> warnings?: string[] }) { this.controllers = options.controllers this.validationWarnings = options.warnings || [] } /** * Public method for parent to set validation result * Called after parent handles the 'validate' event */ public setValidationResult( walletData: WalletData | null, options?: { error?: string warnings?: string[] info?: string[] } ) { this.isRestoring = false if (options?.error) { this.passwordErrors = [options.error] this.validationWarnings = [] this.validationInfo = [] } else if (walletData) { this.walletData = walletData this.validationWarnings = options?.warnings || [] this.validationInfo = options?.info || [] this.currentStep = Step.SUCCESS } } render() { return html`

${this.getStepTitle()}

${this.renderStep()}
` } private renderStep() { switch (this.currentStep) { case Step.UPLOAD: return this.renderUploadStep() case Step.PASSWORD: return this.renderPasswordStep() case Step.PREVIEW: return this.renderPreviewStep() case Step.SUCCESS: return this.renderSuccessStep() default: return nothing } } private renderUploadStep() { return html`

Select your backup file to restore your wallet data. Your backup file stays on your device and is never uploaded.

Drag and drop your backup file here

or

{ e.stopPropagation() this.triggerFileInput() }} > Choose File ${ this.selectedFile ? html`

${this.selectedFile.name}

` : nothing } ${ this.fileError ? html`

${this.fileError}

` : nothing }
Cancel
` } private renderPasswordStep() { const canProceed = this.password && this.password.length > 0 return html`

This backup is encrypted. Enter the password to decrypt it.

{ this.password = e.detail.value }} @keyup=${(e: KeyboardEvent) => e.key === 'Enter' && canProceed && this.previewRestore()} is-full-width autofocus > ${this.passwordErrors.map( (error) => html`
${error}
` )}
Cancel Back
Continue
` } private renderPreviewStep() { return html`

${ this.controllers.length > 0 ? `Found ${this.controllers.length} controller${this.controllers.length === 1 ? '' : 's'} in backup.` : 'No controllers found in backup.' }

${ this.controllers.length > 0 ? html`
Controllers:
${this.controllers.map( (controller) => html`
${this.truncateAddress(controller.address)}
${controller.status === 'new' ? 'NEW' : 'EXISTS'}
${controller.address}
` )}
` : nothing } ${ this.validationWarnings.length > 0 ? html`
${this.validationWarnings.map( (warning) => html`
⚠️ ${warning}
` )}
` : nothing }
Cancel Back
Confirm Restore
` } private renderSuccessStep() { return html`

Wallet Restored Successfully!

${ this.restoredAddresses.length > 0 ? `Imported ${this.restoredAddresses.length} address${this.restoredAddresses.length > 1 ? 'es' : ''} to your wallet.` : 'Your wallet has been restored successfully.' }

${ this.restoredAddresses.length > 0 ? html`
Restored Addresses:
${this.restoredAddresses.map( (address: string) => html`
${this.truncateAddress(address)}
` )}
` : '' }
OK
` } private getStepTitle(): string { switch (this.currentStep) { case Step.UPLOAD: return 'Restore Wallet' case Step.PASSWORD: return 'Enter Password' case Step.PREVIEW: return 'Review Changes' case Step.SUCCESS: return 'Restore Complete' default: return 'Restore Wallet' } } private triggerFileInput() { const input = this.shadowRoot?.querySelector( 'input[type="file"]' ) as HTMLInputElement if (input) { input.click() } } private async handleFileSelect(e: Event) { const input = e.target as HTMLInputElement const file = input.files?.[0] if (file) { await this.handleFile(file) } } private handleDragOver(e: DragEvent) { e.preventDefault() this.isDragging = true } private handleDragLeave() { this.isDragging = false } private async handleDrop(e: DragEvent) { e.preventDefault() this.isDragging = false const file = e.dataTransfer?.files?.[0] if (file) { await this.handleFile(file) } } private async handleFile(file: File) { this.fileError = null if (!file.name.endsWith('.json')) { this.fileError = 'Please select a valid JSON backup file' return } try { // Use passkey-auth readBackupFile function const backup = await readBackupFile(file) this.selectedFile = file this.backupData = backup // Check if encrypted (handle both current and legacy formats) const isEncrypted = backup.encryptedSecrets // Legacy: always encrypted if encryptedSecrets exists ? true : backup.secrets?.encrypted // Current: check encrypted flag if (isEncrypted) { this.currentStep = Step.PASSWORD } else { // Unencrypted backup - preview directly await this.previewRestore() } } catch (error) { this.fileError = error instanceof Error ? error.message : 'Failed to read backup file' console.error('Failed to read backup file:', error) } } private async previewRestore() { if (!this.backupData) return this.isRestoring = true this.passwordErrors = [] try { // First, we need to decrypt the backup if it's encrypted let decryptedBackup = this.backupData if ( this.backupData.encryptedSecrets || this.backupData.secrets?.encrypted ) { // PREVIEW ONLY: Use verify to check password and get preview data (don't actually import yet) if (!this.keyGenerator) { throw new Error('KeyGenerator not available') } // Use verifyBackup for preview - this validates password and returns public data only const verifiedBackup = await this.keyGenerator.verifyBackup( this.backupData, this.password ) // Use the verified backup data (which has filtered public data) decryptedBackup = verifiedBackup } else { // Unencrypted - use data directly for preview (no import needed) decryptedBackup = this.backupData } // Extract all controller addresses from the backup const backupControllers = new Set() // Get addresses from the secrets (the actual private keys we're restoring) const secretsData = (decryptedBackup.secrets?.data as any[]) || [] for (const secret of secretsData) { if (secret.address) { backupControllers.add(secret.address.toLowerCase()) } } // Also get controllers from account metadata (V2 format with networks) for (const account of decryptedBackup.accounts || []) { // V2 format: accounts have networks array with controllers if (account.networks) { for (const network of account.networks) { for (const controller of network.controllers || []) { if (controller.address) { backupControllers.add(controller.address.toLowerCase()) } } } } } // Also get controllers from LSP23CrossChainDeployment if present const lsp23Deployments = (decryptedBackup as any).LSP23CrossChainDeployment || [] for (const deployment of lsp23Deployments) { for (const controller of deployment.initialControllers || []) { if (controller.address) { backupControllers.add(controller.address.toLowerCase()) } } } console.log( '🔍 [RestoreModal] Found controller addresses:', Array.from(backupControllers) ) // Extract addresses from the decrypted backup for preview for (const secret of secretsData) { if (secret.address) { backupControllers.add(secret.address.toLowerCase()) } } // For now, mark all addresses as 'new' - could enhance this later this.controllers = Array.from(backupControllers).map((address) => ({ address, status: 'new' as const, })) this.currentStep = Step.PREVIEW } catch (error) { console.error('Failed to preview restore:', error) const errorMessage = error instanceof Error ? error.message : 'Failed to preview restore' // Check for password-specific errors if (errorMessage.toLowerCase().includes('password')) { this.passwordErrors = ['Incorrect password. Please try again.'] } else { // For non-password errors, set as file error to show in upload step this.fileError = errorMessage this.currentStep = Step.UPLOAD // Clear the file selection so user can try again this.selectedFile = null this.backupData = null // Clear the file input const input = this.shadowRoot?.querySelector( 'input[type="file"]' ) as HTMLInputElement if (input) { input.value = '' } } } finally { this.isRestoring = false } } private async confirmRestore() { if (!this.backupData || !this.keyGenerator) { console.error( '❌ [RestoreModal] Missing backup data or keyGenerator for restore' ) this.dispatchEvent( new CustomEvent('error', { detail: { error: 'Missing backup data or keyGenerator' }, bubbles: true, composed: true, }) ) return } try { this.isRestoring = true console.log('🔄 [RestoreModal] Starting secure restoration process...') // ✅ SECURE: Use keyGenerator.restoreFromBackup internally const result = await this.keyGenerator.restoreFromBackup( this.backupData, this.password ) if (result.success) { console.log('✅ [RestoreModal] Restore successful:', result.addresses) // Store restored addresses for display on success screen this.restoredAddresses = result.addresses // Emit success event (only public addresses, no sensitive data) this.dispatchEvent( new CustomEvent('success', { detail: { addresses: result.addresses }, bubbles: true, composed: true, }) ) // Move to success step this.currentStep = Step.SUCCESS } else { console.error('❌ [RestoreModal] Restore failed') this.dispatchEvent( new CustomEvent('error', { detail: { error: 'Restore failed. Please check the backup file and password.', }, bubbles: true, composed: true, }) ) } } catch (error) { console.error('❌ [RestoreModal] Restore error:', error) const errorMessage = error instanceof Error ? error.message : 'Unknown error' this.dispatchEvent( new CustomEvent('error', { detail: { error: errorMessage }, bubbles: true, composed: true, }) ) } finally { this.isRestoring = false } } private goBack() { if (this.currentStep === Step.PASSWORD) { this.currentStep = Step.UPLOAD this.password = '' this.passwordErrors = [] } } private goBackFromPreview() { // Go back to password step (or upload if unencrypted) const isEncrypted = this.backupData?.encryptedSecrets ? true : this.backupData?.secrets?.encrypted if (isEncrypted) { this.currentStep = Step.PASSWORD } else { this.currentStep = Step.UPLOAD } // Clear preview data this.controllers = [] this.validationWarnings = [] } private close() { this.isOpen = false this.dispatchEvent( new CustomEvent('close', { bubbles: true, composed: true }) ) // Reset state after animation setTimeout(() => { this.currentStep = Step.UPLOAD this.selectedFile = null this.backupData = null this.fileError = null this.isDragging = false this.password = '' this.passwordErrors = [] this.isRestoring = false this.walletData = null this.restoredAddresses = [] this.controllers = [] this.validationWarnings = [] }, 300) } private truncateAddress(address: string): string { if (!address || address.length < 10) return address return `${address.slice(0, 6)}...${address.slice(-4)}` } } declare global { interface HTMLElementTagNameMap { 'restore-modal': RestoreModal } }