/** * Backup Modal - Lit Component * * Framework-agnostic backup modal for wallet data export. * Uses lukso-modal from @lukso/web-components for consistent UI. */ import { safeCustomElement } from '@lukso/core/utils' import type { BackupFile } from '@lukso/passkey-auth' import { downloadBackup } from '@lukso/passkey-auth' import { html, nothing } from 'lit' import { property, state } from 'lit/decorators.js' import zxcvbn from 'zxcvbn' 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 { CHOICE = 1, PASSWORD = 2, DOWNLOAD = 3, } interface PasswordStrength { level: string label: string color: string bgColor: string width: number feedback: string crackTime: string | number } @safeCustomElement('backup-modal') export class BackupModal extends CoreLitElement { // Public properties @property({ type: Boolean, reflect: true }) open = false @property({ type: String }) theme: 'light' | 'dark' | 'auto' = 'auto' // Private state - wizard flow @state() private currentStep: Step = Step.CHOICE @state() private useEncryption = true @state() private password = '' @state() private passwordConfirm = '' @state() private passwordErrors: string[] = [] @state() private isCreatingBackup = false @state() private generatedBackup: BackupFile | null = null /** * Public method for parent to set backup result * Called after parent handles the 'create' event */ public setBackupResult( backupFile: BackupFile | null, options?: { error?: string } ) { this.isCreatingBackup = false if (options?.error) { this.passwordErrors = [options.error] this.generatedBackup = null } else if (backupFile) { this.generatedBackup = backupFile this.currentStep = Step.DOWNLOAD } } render() { return html`

${this.getStepTitle()}

${this.renderStep()}
` } private renderStep() { switch (this.currentStep) { case Step.CHOICE: return this.renderChoiceStep() case Step.PASSWORD: return this.renderPasswordStep() case Step.DOWNLOAD: return this.renderDownloadStep() default: return nothing } } private renderChoiceStep() { return html`
this.selectBackupType(true)} >
Encrypt backup file Recommended

Uses password-based encryption to protect the secrets in this file.

this.selectBackupType(false)} >
Plain text backup For development only

Secrets will be stored in plain text.

Cancel
` } private renderPasswordStep() { const passwordStrength = this.password ? this.calculatePasswordStrength() : null const passwordsMatch = this.password && this.passwordConfirm && this.password === this.passwordConfirm const canProceed = this.password && this.passwordConfirm && passwordsMatch return html`

Choose a strong password to encrypt your backup. You'll need this password to restore your wallet.

{ this.password = e.detail.value }} is-full-width autofocus > ${this.passwordErrors.map( (error) => html`
${error}
` )} ${ passwordStrength ? html`
Password strength: ${passwordStrength.label}

Time to crack: ${passwordStrength.crackTime}

${ passwordStrength.feedback ? html`

${passwordStrength.feedback}

` : nothing }
` : nothing } { this.passwordConfirm = e.detail.value }} is-full-width > ${ this.passwordConfirm ? html`
Passwords match: ${passwordsMatch ? 'Yes' : 'No'}
` : nothing }
Cancel Back
Create Backup
` } private renderDownloadStep() { return html`

Your backup has been created${this.useEncryption ? ' and encrypted' : ''}. Download it and keep it in a safe place.

${ !this.useEncryption ? html`

This unencrypted backup is intended for development purposes. Use it when creating dapps that need access to the profile for testing.

` : nothing }
Download Backup
` } private getStepTitle(): string { switch (this.currentStep) { case Step.CHOICE: return 'Backup Wallet' case Step.PASSWORD: return 'Set Password' case Step.DOWNLOAD: return 'Download Backup' default: return 'Backup Wallet' } } private selectBackupType(encrypted: boolean) { this.useEncryption = encrypted if (encrypted) { this.currentStep = Step.PASSWORD } else { // Unencrypted backup - create immediately this.createBackup() } } private async createBackup() { this.isCreatingBackup = true this.passwordErrors = [] // Emit create event with password (if encrypted) // Parent should handle backup creation const createEvent = new CustomEvent('create', { detail: { password: this.useEncryption ? this.password : undefined, encrypted: this.useEncryption, }, bubbles: true, composed: true, cancelable: true, }) this.dispatchEvent(createEvent) // If parent didn't prevent default, show error if (!createEvent.defaultPrevented) { this.passwordErrors = [ 'No backup handler configured. Please handle the "create" event.', ] this.isCreatingBackup = false } // Otherwise, parent will call setBackupResult() } private handleDownload() { if (this.generatedBackup) { downloadBackup(this.generatedBackup) // Dispatch download event this.dispatchEvent( new CustomEvent('download', { detail: this.generatedBackup, bubbles: true, composed: true, }) ) // Close modal after download setTimeout(() => { this.close() }, 100) } } private goBack() { if (this.currentStep === Step.PASSWORD) { this.currentStep = Step.CHOICE this.password = '' this.passwordConfirm = '' this.passwordErrors = [] } } private close() { this.open = false this.dispatchEvent( new CustomEvent('close', { bubbles: true, composed: true }) ) // Reset state after animation setTimeout(() => { this.currentStep = Step.CHOICE this.useEncryption = true this.password = '' this.passwordConfirm = '' this.passwordErrors = [] this.isCreatingBackup = false this.generatedBackup = null }, 300) } private calculatePasswordStrength(): PasswordStrength { const result = zxcvbn(this.password) const strengthMap = [ { level: 'very-weak', label: 'Very Weak', color: 'text-red-55', bgColor: 'bg-red-55', width: 20, }, { level: 'weak', label: 'Weak', color: 'text-red-65', bgColor: 'bg-red-65', width: 40, }, { level: 'fair', label: 'Fair', color: 'text-yellow-55', bgColor: 'bg-yellow-55', width: 60, }, { level: 'good', label: 'Good', color: 'text-green-54', bgColor: 'bg-green-54', width: 80, }, { level: 'strong', label: 'Strong', color: 'text-green-45', bgColor: 'bg-green-45', width: 100, }, ] const strength = strengthMap[result.score] const feedback = result.feedback.suggestions.join(' ') || result.feedback.warning || '' const crackTime = result.crack_times_display.offline_slow_hashing_1e4_per_second return { ...strength, feedback, crackTime, } } } declare global { interface HTMLElementTagNameMap { 'backup-modal': BackupModal } }