diff --git a/frontend/src/components/migration/ChooseHandleStep.svelte b/frontend/src/components/migration/ChooseHandleStep.svelte
index 71f19f2..7d50dfe 100644
--- a/frontend/src/components/migration/ChooseHandleStep.svelte
+++ b/frontend/src/components/migration/ChooseHandleStep.svelte
@@ -1,34 +1,44 @@
{$_('migration.inbound.emailVerify.title')}
-
{@html $_('migration.inbound.emailVerify.desc', { values: { email: `${email}` } })}
-
-
- {$_('migration.inbound.emailVerify.hint')}
-
-
-
- {#if error}
-
- {error}
+ {#if isTelegram && telegramBotUsername && handle}
+ {@const encodedHandle = handle.replaceAll('.', '_')}
+
{$_('migration.inbound.emailVerify.telegramInstructions')}
+
+
{$_('migration.inbound.emailVerify.waitingForVerification')}
+ {:else if isDiscord && discordAppId && handle}
+
{$_('migration.inbound.emailVerify.discordInstructions')}
+
+
{$_('migration.inbound.emailVerify.waitingForVerification')}
+ {:else}
+
{@html $_('migration.inbound.emailVerify.desc', { values: { email: `${identifier}`, channel: channelLabel(channel) } })}
+
+
+
+ {$_('migration.inbound.emailVerify.hint')}
+
+
+
+ {#if error}
+
+ {error}
+
+ {/if}
+
+
{/if}
-
-
diff --git a/frontend/src/components/migration/InboundWizard.svelte b/frontend/src/components/migration/InboundWizard.svelte
index 3e363f9..73a1244 100644
--- a/frontend/src/components/migration/InboundWizard.svelte
+++ b/frontend/src/components/migration/InboundWizard.svelte
@@ -1,8 +1,9 @@
-
- {#each steps as _, i}
-
-
{i < getCurrentStepIndex() ? '✓' : i + 1}
-
- {#if i < steps.length - 1}
-
- {/if}
- {/each}
-
-
- {steps[getCurrentStepIndex()]} · Step {getCurrentStepIndex() + 1} of {steps.length}
-
+
{#if flow.state.error}
{flow.state.error}
@@ -443,29 +398,37 @@
flow.checkHandleAvailability(h)}
onHandleChange={(h) => handleInput = h}
onDomainChange={(d) => selectedDomain = d}
- onCheckHandle={checkHandle}
onEmailChange={(e) => flow.updateField('targetEmail', e)}
onPasswordChange={(p) => flow.updateField('targetPassword', p)}
onAuthMethodChange={(m) => selectedAuthMethod = m}
onInviteCodeChange={(c) => flow.updateField('inviteCode', c)}
+ onVerificationChannelChange={(ch) => flow.updateField('verificationChannel', ch)}
+ onDiscordChange={(v) => flow.updateField('discordUsername', v)}
+ onTelegramChange={(v) => flow.updateField('telegramUsername', v)}
+ onSignalChange={(v) => flow.updateField('signalUsername', v)}
onHandlePreservationChange={handlePreservationChange}
onVerifyExistingHandle={verifyExistingHandle}
onBack={() => flow.setStep('source-handle')}
@@ -473,88 +436,39 @@
/>
{:else if flow.state.step === 'review'}
-
-
{$_('migration.inbound.review.title')}
-
{$_('migration.inbound.review.desc')}
-
-
-
- {$_('migration.inbound.review.currentHandle')}:
- {flow.state.sourceHandle}
-
-
- {$_('migration.inbound.review.newHandle')}:
- {flow.state.targetHandle}
-
-
- {$_('migration.inbound.review.did')}:
- {flow.state.sourceDid}
-
-
- {$_('migration.inbound.review.sourcePds')}:
- {flow.state.sourcePdsUrl}
-
-
- {$_('migration.inbound.review.targetPds')}:
- {window.location.origin}
-
-
- {$_('migration.inbound.review.email')}:
- {flow.state.targetEmail}
-
-
- {$_('migration.inbound.review.authentication')}:
- {flow.state.authMethod === 'passkey' ? $_('migration.inbound.review.authPasskey') : $_('migration.inbound.review.authPassword')}
-
-
-
-
+ flow.setStep('choose-handle')}
+ onContinue={startMigration}
+ >
+ {#snippet warning()}
{$_('migration.inbound.review.warning')}
-
-
-
-
-
-
-
+ {/snippet}
+
{:else if flow.state.step === 'migrating'}
-
-
{$_('migration.inbound.migrating.title')}
-
{$_('migration.inbound.migrating.desc')}
-
-
-
- {flow.state.progress.repoExported ? '✓' : '○'}
- {$_('migration.inbound.migrating.exportRepo')}
-
-
- {flow.state.progress.repoImported ? '✓' : '○'}
- {$_('migration.inbound.migrating.importRepo')}
-
-
- {flow.state.progress.blobsMigrated === flow.state.progress.blobsTotal && flow.state.progress.blobsTotal > 0 ? '✓' : '○'}
- {$_('migration.inbound.migrating.migrateBlobs')} ({flow.state.progress.blobsMigrated}/{flow.state.progress.blobsTotal})
-
-
- {flow.state.progress.prefsMigrated ? '✓' : '○'}
- {$_('migration.inbound.migrating.migratePrefs')}
-
-
-
- {#if flow.state.progress.blobsTotal > 0}
-
- {/if}
-
-
{flow.state.progress.currentOperation}
-
+ 0, active: flow.state.progress.repoImported && !flow.state.progress.prefsMigrated },
+ { label: $_('migration.inbound.migrating.migratePrefs'), completed: flow.state.progress.prefsMigrated },
+ ]}
+ statusText={flow.state.progress.currentOperation}
+ progressBar={flow.state.progress.blobsTotal > 0 ? { current: flow.state.progress.blobsMigrated, total: flow.state.progress.blobsTotal } : undefined}
+ />
{:else if flow.state.step === 'passkey-setup'}
{:else if flow.state.step === 'finalizing'}
-
-
{$_('migration.inbound.finalizing.title')}
-
{$_('migration.inbound.finalizing.desc')}
-
-
-
- {flow.state.progress.plcSigned ? '✓' : '○'}
- {$_('migration.inbound.finalizing.signingPlc')}
-
-
- {flow.state.progress.activated ? '✓' : '○'}
- {$_('migration.inbound.finalizing.activating')}
-
-
- {flow.state.progress.deactivated ? '✓' : '○'}
- {$_('migration.inbound.finalizing.deactivating')}
-
-
-
-
{flow.state.progress.currentOperation}
-
+
{:else if flow.state.step === 'success'}
diff --git a/frontend/src/components/migration/OfflineInboundWizard.svelte b/frontend/src/components/migration/OfflineInboundWizard.svelte
index 6df2922..177c532 100644
--- a/frontend/src/components/migration/OfflineInboundWizard.svelte
+++ b/frontend/src/components/migration/OfflineInboundWizard.svelte
@@ -1,8 +1,9 @@
-
- {#each steps as _, i}
-
-
{i < getCurrentStepIndex() ? '✓' : i + 1}
-
- {#if i < steps.length - 1}
-
- {/if}
- {/each}
-
-
- {steps[getCurrentStepIndex()]} · Step {getCurrentStepIndex() + 1} of {steps.length}
-
+
{#if flow.state.error}
{flow.state.error}
@@ -401,13 +377,16 @@
flow.checkHandleAvailability(h)}
onHandleChange={(h) => handleInput = h}
onDomainChange={(d) => selectedDomain = d}
- onCheckHandle={checkHandle}
onEmailChange={(e) => flow.setTargetEmail(e)}
onPasswordChange={(p) => flow.setTargetPassword(p)}
onAuthMethodChange={(m) => selectedAuthMethod = m}
onInviteCodeChange={(c) => flow.setInviteCode(c)}
+ onVerificationChannelChange={(ch) => flow.updateField('verificationChannel', ch)}
+ onDiscordChange={(v) => flow.updateField('discordUsername', v)}
+ onTelegramChange={(v) => flow.updateField('telegramUsername', v)}
+ onSignalChange={(v) => flow.updateField('signalUsername', v)}
onBack={() => flow.setStep('provide-rotation-key')}
onContinue={proceedToReview}
/>
{:else if flow.state.step === 'review'}
-
-
{$_('migration.inbound.review.title')}
-
{$_('migration.offline.review.desc')}
-
-
-
- {$_('migration.inbound.review.did')}:
- {flow.state.userDid}
-
-
- {$_('migration.inbound.review.newHandle')}:
- {flow.state.targetHandle}
-
-
- {$_('migration.offline.review.carFile')}:
- {flow.state.carFileName} ({(flow.state.carSizeBytes / 1024 / 1024).toFixed(2)} MB)
-
-
- {$_('migration.offline.review.rotationKey')}:
- {flow.state.rotationKeyDidKey}
-
-
- {$_('migration.inbound.review.targetPds')}:
- {window.location.origin}
-
-
- {$_('migration.inbound.review.email')}:
- {flow.state.targetEmail}
-
-
- {$_('migration.inbound.review.authentication')}:
- {flow.state.authMethod === 'passkey' ? $_('migration.inbound.review.authPasskey') : $_('migration.inbound.review.authPassword')}
-
-
-
-
+
flow.setStep('choose-handle')}
+ onContinue={startMigration}
+ >
+ {#snippet warning()}
{$_('migration.offline.review.plcWarningTitle')}
{$_('migration.offline.review.plcWarning')}
-
-
-
-
-
-
-
+ {/snippet}
+
{:else if flow.state.step === 'creating' || flow.state.step === 'importing'}
-
-
{$_('migration.offline.migrating.title')}
-
{$_('migration.offline.migrating.desc')}
-
-
-
- {flow.state.step !== 'creating' ? '✓' : '○'}
- {$_('migration.offline.migrating.creating')}
-
-
- ○
- {$_('migration.offline.migrating.importing')}
-
-
-
-
{flow.state.progress.currentOperation}
-
+
{:else if flow.state.step === 'migrating-blobs'}
-
-
{$_('migration.offline.blobs.title')}
-
{$_('migration.offline.blobs.desc')}
-
-
-
- ✓
- {$_('migration.offline.migrating.importing')}
-
-
- ○
- {$_('migration.offline.blobs.migrating')}
-
-
-
- {#if flow.state.progress.blobsTotal > 0}
-
-
-
- {flow.state.progress.blobsMigrated} / {flow.state.progress.blobsTotal} blobs
-
-
- {/if}
-
-
{flow.state.progress.currentOperation}
-
+
0 ? { current: flow.state.progress.blobsMigrated, total: flow.state.progress.blobsTotal } : undefined}
+ >
{#if flow.state.progress.blobsFailed.length > 0}
{$_('migration.offline.blobs.failedTitle')}
{$_('migration.offline.blobs.failedDesc', { values: { count: flow.state.progress.blobsFailed.length } })}
{/if}
-
+
{:else if flow.state.step === 'email-verify'}
{:else if flow.state.step === 'plc-signing' || flow.state.step === 'finalizing'}
-
-
{$_('migration.inbound.finalizing.title')}
-
{$_('migration.inbound.finalizing.desc')}
-
-
-
- {flow.state.progress.plcSigned ? '✓' : '○'}
- {$_('migration.inbound.finalizing.signingPlc')}
-
-
- {flow.state.progress.activated ? '✓' : '○'}
- {$_('migration.inbound.finalizing.activating')}
-
-
-
-
{flow.state.progress.currentOperation}
-
+
{:else if flow.state.step === 'success'}
+ import type { Snippet } from 'svelte'
+
+ interface ProgressItem {
+ label: string
+ completed: boolean
+ active?: boolean
+ }
+
+ interface Props {
+ title: string
+ description: string
+ items: ProgressItem[]
+ statusText: string
+ progressBar?: { current: number; total: number }
+ children?: Snippet
+ }
+
+ let { title, description, items, statusText, progressBar, children }: Props = $props()
+
+
+
+
{title}
+
{description}
+
+
+ {#each items as item}
+
+ {item.completed ? '✓' : '○'}
+ {item.label}
+
+ {/each}
+
+
+ {#if progressBar && progressBar.total > 0}
+
+ {/if}
+
+
{statusText}
+
+ {#if children}
+ {@render children()}
+ {/if}
+
diff --git a/frontend/src/components/migration/ReviewStep.svelte b/frontend/src/components/migration/ReviewStep.svelte
new file mode 100644
index 0000000..0668853
--- /dev/null
+++ b/frontend/src/components/migration/ReviewStep.svelte
@@ -0,0 +1,48 @@
+
+
+
+
{$_('migration.inbound.review.title')}
+
{description}
+
+
+ {#each rows as row}
+
+ {row.label}:
+ {row.value}
+
+ {/each}
+
+
+ {#if warning}
+
+ {@render warning()}
+
+ {/if}
+
+
+
+
+
+
diff --git a/frontend/src/components/migration/StepIndicator.svelte b/frontend/src/components/migration/StepIndicator.svelte
new file mode 100644
index 0000000..691393f
--- /dev/null
+++ b/frontend/src/components/migration/StepIndicator.svelte
@@ -0,0 +1,22 @@
+
+
+
+ {#each steps as _, i}
+
+
{i < currentIndex ? '✓' : i + 1}
+
+ {#if i < steps.length - 1}
+
+ {/if}
+ {/each}
+
+
+ {steps[currentIndex]} · Step {currentIndex + 1} of {steps.length}
+
diff --git a/frontend/src/styles/migration.css b/frontend/src/styles/migration.css
index 12a0b92..7bf6656 100644
--- a/frontend/src/styles/migration.css
+++ b/frontend/src/styles/migration.css
@@ -92,12 +92,6 @@
margin: 0 0 var(--space-5) 0;
}
-.info-box {
- background: var(--accent-muted);
- padding: var(--space-5);
- margin-bottom: var(--space-5);
-}
-
.info-box h3 {
margin: 0 0 var(--space-3) 0;
font-size: var(--text-base);
@@ -119,13 +113,6 @@
color: var(--text-secondary);
}
-.warning-box {
- background: var(--warning-bg);
- padding: var(--space-5);
- margin-bottom: var(--space-5);
- font-size: var(--text-sm);
-}
-
.warning-box strong {
color: var(--warning-text);
}
@@ -158,6 +145,7 @@
.button-row {
display: flex;
+ flex-direction: row;
gap: var(--space-3);
justify-content: flex-end;
margin-top: var(--space-5);
@@ -260,28 +248,6 @@
font-size: var(--text-sm);
}
-.blob-progress {
- margin: var(--space-4) 0;
-}
-
-.blob-progress-bar {
- height: 8px;
- background: var(--bg-primary);
- overflow: hidden;
- margin-bottom: var(--space-2);
-}
-
-.blob-progress-fill {
- height: 100%;
- background: var(--accent);
-}
-
-.blob-progress-text {
- text-align: center;
- color: var(--text-secondary);
- font-size: var(--text-sm);
- margin: 0;
-}
.success-content {
text-align: center;
@@ -411,43 +377,6 @@ label.auth-option {
color: var(--text-secondary);
}
-.app-password-display {
- background: var(--bg-primary);
- padding: var(--space-5);
- margin-bottom: var(--space-5);
- text-align: center;
-}
-
-.app-password-label {
- font-size: var(--text-sm);
- color: var(--text-secondary);
- margin-bottom: var(--space-3);
-}
-
-.app-password-code {
- display: block;
- font-family: var(--font-mono);
- font-size: var(--text-lg);
- letter-spacing: 0.1em;
- padding: var(--space-4);
- background: var(--bg-tertiary);
- margin-bottom: var(--space-4);
- user-select: all;
-}
-
-.copy-btn {
- font-size: var(--text-sm);
-}
-
-.current-account {
- background: var(--bg-primary);
- padding: var(--space-4);
- margin-bottom: var(--space-5);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
.current-account .label {
color: var(--text-secondary);
}
@@ -457,12 +386,6 @@ label.auth-option {
font-size: var(--text-lg);
}
-.server-info {
- background: var(--bg-primary);
- padding: var(--space-4);
- margin-top: var(--space-5);
-}
-
.server-info h3 {
margin: 0 0 var(--space-3) 0;
font-size: var(--text-base);
@@ -488,11 +411,6 @@ label.auth-option {
font-size: var(--text-sm);
}
-.final-warning {
- background: var(--error-bg);
- border-color: var(--error-border);
-}
-
.final-warning strong {
color: var(--error-text);
}
@@ -596,16 +514,6 @@ label.auth-option {
margin-bottom: var(--space-4);
}
-.message.success {
- background: var(--success-bg);
- color: var(--success-text);
-}
-
-.message.error {
- background: var(--error-bg);
- color: var(--error-text);
-}
-
.handle-choice-options {
display: flex;
flex-direction: column;