/**
* 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
}
}