this repo has no description
1<script lang="ts"> 2 import type { OutboundMigrationFlow } from '../../lib/migration' 3 import type { ServerDescription } from '../../lib/migration/types' 4 import { getAuthState, logout } from '../../lib/auth.svelte' 5 import '../../styles/migration.css' 6 7 interface Props { 8 flow: OutboundMigrationFlow 9 onBack: () => void 10 onComplete: () => void 11 } 12 13 let { flow, onBack, onComplete }: Props = $props() 14 15 const auth = getAuthState() 16 17 let loading = $state(false) 18 let understood = $state(false) 19 let pdsUrlInput = $state('') 20 let handleInput = $state('') 21 let selectedDomain = $state('') 22 let confirmFinal = $state(false) 23 24 $effect(() => { 25 if (flow.state.step === 'success') { 26 setTimeout(async () => { 27 await logout() 28 onComplete() 29 }, 3000) 30 } 31 }) 32 33 $effect(() => { 34 if (flow.state.targetServerInfo?.availableUserDomains?.length) { 35 selectedDomain = flow.state.targetServerInfo.availableUserDomains[0] 36 } 37 }) 38 39 async function validatePds(e: Event) { 40 e.preventDefault() 41 loading = true 42 flow.updateField('error', null) 43 44 try { 45 let url = pdsUrlInput.trim() 46 if (!url.startsWith('http://') && !url.startsWith('https://')) { 47 url = `https://${url}` 48 } 49 await flow.validateTargetPds(url) 50 flow.setStep('new-account') 51 } catch (err) { 52 flow.setError((err as Error).message) 53 } finally { 54 loading = false 55 } 56 } 57 58 function proceedToReview() { 59 const fullHandle = handleInput.includes('.') 60 ? handleInput 61 : `${handleInput}.${selectedDomain}` 62 63 flow.updateField('targetHandle', fullHandle) 64 flow.setStep('review') 65 } 66 67 async function startMigration() { 68 if (!auth.session) return 69 loading = true 70 try { 71 await flow.startMigration(auth.session.did) 72 } catch (err) { 73 flow.setError((err as Error).message) 74 } finally { 75 loading = false 76 } 77 } 78 79 async function submitPlcToken(e: Event) { 80 e.preventDefault() 81 loading = true 82 try { 83 await flow.submitPlcToken(flow.state.plcToken) 84 } catch (err) { 85 flow.setError((err as Error).message) 86 } finally { 87 loading = false 88 } 89 } 90 91 async function resendToken() { 92 loading = true 93 try { 94 await flow.resendPlcToken() 95 flow.setError(null) 96 } catch (err) { 97 flow.setError((err as Error).message) 98 } finally { 99 loading = false 100 } 101 } 102 103 function isDidWeb(): boolean { 104 return auth.session?.did?.startsWith('did:web:') ?? false 105 } 106 107 const steps = ['Target', 'Setup', 'Review', 'Transfer', 'Verify', 'Complete'] 108 function getCurrentStepIndex(): number { 109 switch (flow.state.step) { 110 case 'welcome': return -1 111 case 'target-pds': return 0 112 case 'new-account': return 1 113 case 'review': return 2 114 case 'migrating': return 3 115 case 'plc-token': 116 case 'finalizing': return 4 117 case 'success': return 5 118 default: return 0 119 } 120 } 121</script> 122 123<div class="migration-wizard"> 124 {#if flow.state.step !== 'welcome'} 125 <div class="step-indicator"> 126 {#each steps as stepName, i} 127 <div class="step" class:active={i === getCurrentStepIndex()} class:completed={i < getCurrentStepIndex()}> 128 <div class="step-dot">{i < getCurrentStepIndex() ? '✓' : i + 1}</div> 129 <span class="step-label">{stepName}</span> 130 </div> 131 {#if i < steps.length - 1} 132 <div class="step-line" class:completed={i < getCurrentStepIndex()}></div> 133 {/if} 134 {/each} 135 </div> 136 {/if} 137 138 {#if flow.state.error} 139 <div class="migration-message error">{flow.state.error}</div> 140 {/if} 141 142 {#if flow.state.step === 'welcome'} 143 <div class="step-content"> 144 <h2>Migrate Your Account Away</h2> 145 <p>This wizard will help you move your AT Protocol account from this PDS to another one.</p> 146 147 <div class="current-account"> 148 <span class="label">Current account:</span> 149 <span class="value">@{auth.session?.handle}</span> 150 </div> 151 152 {#if isDidWeb()} 153 <div class="migration-warning-box"> 154 <strong>did:web Migration Notice</strong> 155 <p> 156 Your account uses a did:web identifier ({auth.session?.did}). After migrating, this PDS will 157 continue serving your DID document with an updated service endpoint pointing to your new PDS. 158 </p> 159 <p> 160 You can return here anytime to update the forwarding if you migrate again in the future. 161 </p> 162 </div> 163 {/if} 164 165 <div class="migration-info-box"> 166 <h3>What will happen:</h3> 167 <ol> 168 <li>Choose your new PDS</li> 169 <li>Set up your account on the new server</li> 170 <li>Your repository and blobs will be transferred</li> 171 <li>Verify the migration via email</li> 172 <li>Your identity will be updated to point to the new PDS</li> 173 <li>Your account here will be deactivated</li> 174 </ol> 175 </div> 176 177 <div class="migration-warning-box"> 178 <strong>Before you proceed:</strong> 179 <ul> 180 <li>You need access to the email registered with this account</li> 181 <li>You will lose access to this account on this PDS</li> 182 <li>Make sure you trust the destination PDS</li> 183 <li>Large accounts may take several minutes to transfer</li> 184 </ul> 185 </div> 186 187 <label class="checkbox-label"> 188 <input type="checkbox" bind:checked={understood} /> 189 <span>I understand that my account will be moved and deactivated here</span> 190 </label> 191 192 <div class="button-row"> 193 <button class="ghost" onclick={onBack}>Cancel</button> 194 <button disabled={!understood} onclick={() => flow.setStep('target-pds')}> 195 Continue 196 </button> 197 </div> 198 </div> 199 200 {:else if flow.state.step === 'target-pds'} 201 <div class="step-content"> 202 <h2>Choose Your New PDS</h2> 203 <p>Enter the URL of the PDS you want to migrate to.</p> 204 205 <form onsubmit={validatePds}> 206 <div class="migration-field"> 207 <label for="pds-url">PDS URL</label> 208 <input 209 id="pds-url" 210 type="text" 211 placeholder="pds.example.com" 212 bind:value={pdsUrlInput} 213 disabled={loading} 214 required 215 /> 216 <p class="migration-hint">The server address of your new PDS (e.g., bsky.social, pds.example.com)</p> 217 </div> 218 219 <div class="button-row"> 220 <button type="button" class="ghost" onclick={() => flow.setStep('welcome')} disabled={loading}>Back</button> 221 <button type="submit" disabled={loading || !pdsUrlInput.trim()}> 222 {loading ? 'Checking...' : 'Connect'} 223 </button> 224 </div> 225 </form> 226 227 {#if flow.state.targetServerInfo} 228 <div class="server-info"> 229 <h3>Connected to PDS</h3> 230 <div class="info-row"> 231 <span class="label">Server:</span> 232 <span class="value">{flow.state.targetPdsUrl}</span> 233 </div> 234 {#if flow.state.targetServerInfo.availableUserDomains.length > 0} 235 <div class="info-row"> 236 <span class="label">Available domains:</span> 237 <span class="value">{flow.state.targetServerInfo.availableUserDomains.join(', ')}</span> 238 </div> 239 {/if} 240 <div class="info-row"> 241 <span class="label">Invite required:</span> 242 <span class="value">{flow.state.targetServerInfo.inviteCodeRequired ? 'Yes' : 'No'}</span> 243 </div> 244 {#if flow.state.targetServerInfo.links?.termsOfService} 245 <a href={flow.state.targetServerInfo.links.termsOfService} target="_blank" rel="noopener"> 246 Terms of Service 247 </a> 248 {/if} 249 {#if flow.state.targetServerInfo.links?.privacyPolicy} 250 <a href={flow.state.targetServerInfo.links.privacyPolicy} target="_blank" rel="noopener"> 251 Privacy Policy 252 </a> 253 {/if} 254 </div> 255 {/if} 256 </div> 257 258 {:else if flow.state.step === 'new-account'} 259 <div class="step-content"> 260 <h2>Set Up Your New Account</h2> 261 <p>Configure your account details on the new PDS.</p> 262 263 <div class="current-info"> 264 <span class="label">Migrating to:</span> 265 <span class="value">{flow.state.targetPdsUrl}</span> 266 </div> 267 268 <div class="migration-field"> 269 <label for="new-handle">New Handle</label> 270 <div class="handle-input-group"> 271 <input 272 id="new-handle" 273 type="text" 274 placeholder="username" 275 bind:value={handleInput} 276 /> 277 {#if flow.state.targetServerInfo && flow.state.targetServerInfo.availableUserDomains.length > 0 && !handleInput.includes('.')} 278 <select bind:value={selectedDomain}> 279 {#each flow.state.targetServerInfo.availableUserDomains as domain} 280 <option value={domain}>.{domain}</option> 281 {/each} 282 </select> 283 {/if} 284 </div> 285 <p class="migration-hint">You can also use your own domain by entering the full handle (e.g., alice.mydomain.com)</p> 286 </div> 287 288 <div class="migration-field"> 289 <label for="email">Email Address</label> 290 <input 291 id="email" 292 type="email" 293 placeholder="you@example.com" 294 bind:value={flow.state.targetEmail} 295 oninput={(e) => flow.updateField('targetEmail', (e.target as HTMLInputElement).value)} 296 required 297 /> 298 </div> 299 300 <div class="migration-field"> 301 <label for="new-password">Password</label> 302 <input 303 id="new-password" 304 type="password" 305 placeholder="Password for your new account" 306 bind:value={flow.state.targetPassword} 307 oninput={(e) => flow.updateField('targetPassword', (e.target as HTMLInputElement).value)} 308 required 309 minlength="8" 310 /> 311 <p class="migration-hint">At least 8 characters. This will be your password on the new PDS.</p> 312 </div> 313 314 {#if flow.state.targetServerInfo?.inviteCodeRequired} 315 <div class="migration-field"> 316 <label for="invite">Invite Code</label> 317 <input 318 id="invite" 319 type="text" 320 placeholder="Enter invite code" 321 bind:value={flow.state.inviteCode} 322 oninput={(e) => flow.updateField('inviteCode', (e.target as HTMLInputElement).value)} 323 required 324 /> 325 <p class="migration-hint">Required by this PDS to create an account</p> 326 </div> 327 {/if} 328 329 <div class="button-row"> 330 <button class="ghost" onclick={() => flow.setStep('target-pds')}>Back</button> 331 <button 332 disabled={!handleInput.trim() || !flow.state.targetEmail || !flow.state.targetPassword} 333 onclick={proceedToReview} 334 > 335 Continue 336 </button> 337 </div> 338 </div> 339 340 {:else if flow.state.step === 'review'} 341 <div class="step-content"> 342 <h2>Review Migration</h2> 343 <p>Please confirm the details of your migration.</p> 344 345 <div class="review-card"> 346 <div class="review-row"> 347 <span class="label">Current Handle:</span> 348 <span class="value">@{auth.session?.handle}</span> 349 </div> 350 <div class="review-row"> 351 <span class="label">New Handle:</span> 352 <span class="value">@{flow.state.targetHandle}</span> 353 </div> 354 <div class="review-row"> 355 <span class="label">DID:</span> 356 <span class="value mono">{auth.session?.did}</span> 357 </div> 358 <div class="review-row"> 359 <span class="label">From PDS:</span> 360 <span class="value">{window.location.origin}</span> 361 </div> 362 <div class="review-row"> 363 <span class="label">To PDS:</span> 364 <span class="value">{flow.state.targetPdsUrl}</span> 365 </div> 366 <div class="review-row"> 367 <span class="label">New Email:</span> 368 <span class="value">{flow.state.targetEmail}</span> 369 </div> 370 </div> 371 372 <div class="migration-warning-box final-warning"> 373 <strong>This action cannot be easily undone!</strong> 374 <p> 375 After migration completes, your account on this PDS will be deactivated. 376 To return, you would need to migrate back from the new PDS. 377 </p> 378 </div> 379 380 <label class="checkbox-label"> 381 <input type="checkbox" bind:checked={confirmFinal} /> 382 <span>I confirm I want to migrate my account to {flow.state.targetPdsUrl}</span> 383 </label> 384 385 <div class="button-row"> 386 <button class="ghost" onclick={() => flow.setStep('new-account')} disabled={loading}>Back</button> 387 <button class="danger" onclick={startMigration} disabled={loading || !confirmFinal}> 388 {loading ? 'Starting...' : 'Start Migration'} 389 </button> 390 </div> 391 </div> 392 393 {:else if flow.state.step === 'migrating'} 394 <div class="step-content"> 395 <h2>Migration in Progress</h2> 396 <p>Please wait while your account is being transferred...</p> 397 398 <div class="progress-section"> 399 <div class="progress-item" class:completed={flow.state.progress.repoExported}> 400 <span class="icon">{flow.state.progress.repoExported ? '✓' : '○'}</span> 401 <span>Export repository</span> 402 </div> 403 <div class="progress-item" class:completed={flow.state.progress.repoImported}> 404 <span class="icon">{flow.state.progress.repoImported ? '✓' : '○'}</span> 405 <span>Import repository to new PDS</span> 406 </div> 407 <div class="progress-item" class:active={flow.state.progress.repoImported && !flow.state.progress.prefsMigrated}> 408 <span class="icon">{flow.state.progress.blobsMigrated === flow.state.progress.blobsTotal && flow.state.progress.blobsTotal > 0 ? '✓' : '○'}</span> 409 <span>Migrate blobs ({flow.state.progress.blobsMigrated}/{flow.state.progress.blobsTotal})</span> 410 </div> 411 <div class="progress-item" class:completed={flow.state.progress.prefsMigrated}> 412 <span class="icon">{flow.state.progress.prefsMigrated ? '✓' : '○'}</span> 413 <span>Migrate preferences</span> 414 </div> 415 </div> 416 417 {#if flow.state.progress.blobsTotal > 0} 418 <div class="progress-bar"> 419 <div 420 class="progress-fill" 421 style="width: {(flow.state.progress.blobsMigrated / flow.state.progress.blobsTotal) * 100}%" 422 ></div> 423 </div> 424 {/if} 425 426 <p class="status-text">{flow.state.progress.currentOperation}</p> 427 </div> 428 429 {:else if flow.state.step === 'plc-token'} 430 <div class="step-content"> 431 <h2>Verify Migration</h2> 432 <p>A verification code has been sent to your email ({auth.session?.email}).</p> 433 434 <div class="migration-info-box"> 435 <p> 436 This code confirms you have access to the account and authorizes updating your identity 437 to point to the new PDS. 438 </p> 439 </div> 440 441 <form onsubmit={submitPlcToken}> 442 <div class="migration-field"> 443 <label for="plc-token">Verification Code</label> 444 <input 445 id="plc-token" 446 type="text" 447 placeholder="Enter code from email" 448 bind:value={flow.state.plcToken} 449 oninput={(e) => flow.updateField('plcToken', (e.target as HTMLInputElement).value)} 450 disabled={loading} 451 required 452 /> 453 </div> 454 455 <div class="button-row"> 456 <button type="button" class="ghost" onclick={resendToken} disabled={loading}> 457 Resend Code 458 </button> 459 <button type="submit" disabled={loading || !flow.state.plcToken}> 460 {loading ? 'Verifying...' : 'Complete Migration'} 461 </button> 462 </div> 463 </form> 464 </div> 465 466 {:else if flow.state.step === 'finalizing'} 467 <div class="step-content"> 468 <h2>Finalizing Migration</h2> 469 <p>Please wait while we complete the migration...</p> 470 471 <div class="progress-section"> 472 <div class="progress-item" class:completed={flow.state.progress.plcSigned}> 473 <span class="icon">{flow.state.progress.plcSigned ? '✓' : '○'}</span> 474 <span>Sign identity update</span> 475 </div> 476 <div class="progress-item" class:completed={flow.state.progress.activated}> 477 <span class="icon">{flow.state.progress.activated ? '✓' : '○'}</span> 478 <span>Activate account on new PDS</span> 479 </div> 480 <div class="progress-item" class:completed={flow.state.progress.deactivated}> 481 <span class="icon">{flow.state.progress.deactivated ? '✓' : '○'}</span> 482 <span>Deactivate account here</span> 483 </div> 484 </div> 485 486 <p class="status-text">{flow.state.progress.currentOperation}</p> 487 </div> 488 489 {:else if flow.state.step === 'success'} 490 <div class="step-content success-content"> 491 <div class="success-icon"></div> 492 <h2>Migration Complete!</h2> 493 <p>Your account has been successfully migrated to your new PDS.</p> 494 495 <div class="success-details"> 496 <div class="detail-row"> 497 <span class="label">Your new handle:</span> 498 <span class="value">@{flow.state.targetHandle}</span> 499 </div> 500 <div class="detail-row"> 501 <span class="label">New PDS:</span> 502 <span class="value">{flow.state.targetPdsUrl}</span> 503 </div> 504 <div class="detail-row"> 505 <span class="label">DID:</span> 506 <span class="value mono">{auth.session?.did}</span> 507 </div> 508 </div> 509 510 {#if flow.state.progress.blobsFailed.length > 0} 511 <div class="migration-warning-box"> 512 <strong>Note:</strong> {flow.state.progress.blobsFailed.length} blobs could not be migrated. 513 These may be images or other media that are no longer available. 514 </div> 515 {/if} 516 517 <div class="next-steps"> 518 <h3>Next Steps</h3> 519 <ol> 520 <li>Visit your new PDS at <a href={flow.state.targetPdsUrl} target="_blank" rel="noopener">{flow.state.targetPdsUrl}</a></li> 521 <li>Log in with your new credentials</li> 522 <li>Your followers and following will continue to work</li> 523 </ol> 524 </div> 525 526 <p class="redirect-text">Logging out in a moment...</p> 527 </div> 528 529 {:else if flow.state.step === 'error'} 530 <div class="step-content"> 531 <h2>Migration Error</h2> 532 <p>An error occurred during migration.</p> 533 534 <div class="migration-error-box"> 535 {flow.state.error} 536 </div> 537 538 <div class="button-row"> 539 <button class="ghost" onclick={onBack}>Start Over</button> 540 </div> 541 </div> 542 {/if} 543</div> 544 545<style> 546</style>