this repo has no description
1<script lang="ts"> 2 import { getAuthState, logout, setSession } from '../lib/auth.svelte' 3 import { navigate } from '../lib/router.svelte' 4 import { _ } from '../lib/i18n' 5 import { 6 createInboundMigrationFlow, 7 createOutboundMigrationFlow, 8 hasPendingMigration, 9 getResumeInfo, 10 clearMigrationState, 11 loadMigrationState, 12 } from '../lib/migration' 13 import InboundWizard from '../components/migration/InboundWizard.svelte' 14 import OutboundWizard from '../components/migration/OutboundWizard.svelte' 15 16 const auth = getAuthState() 17 18 type Direction = 'select' | 'inbound' | 'outbound' 19 let direction = $state<Direction>('select') 20 let showResumeModal = $state(false) 21 let resumeInfo = $state<ReturnType<typeof getResumeInfo>>(null) 22 let oauthError = $state<string | null>(null) 23 let oauthLoading = $state(false) 24 25 let inboundFlow = $state<ReturnType<typeof createInboundMigrationFlow> | null>(null) 26 let outboundFlow = $state<ReturnType<typeof createOutboundMigrationFlow> | null>(null) 27 let oauthCallbackProcessed = $state(false) 28 29 $effect(() => { 30 if (oauthCallbackProcessed) return 31 32 const url = new URL(window.location.href) 33 const code = url.searchParams.get('code') 34 const state = url.searchParams.get('state') 35 const errorParam = url.searchParams.get('error') 36 const errorDescription = url.searchParams.get('error_description') 37 38 if (errorParam) { 39 oauthCallbackProcessed = true 40 oauthError = errorDescription || errorParam 41 window.history.replaceState({}, '', '/#/migrate') 42 return 43 } 44 45 if (code && state) { 46 oauthCallbackProcessed = true 47 window.history.replaceState({}, '', '/#/migrate') 48 direction = 'inbound' 49 oauthLoading = true 50 inboundFlow = createInboundMigrationFlow() 51 52 inboundFlow.handleOAuthCallback(code, state) 53 .then(() => { 54 oauthLoading = false 55 }) 56 .catch((e) => { 57 oauthLoading = false 58 oauthError = e.message || 'OAuth authentication failed' 59 inboundFlow = null 60 direction = 'select' 61 }) 62 return 63 } 64 }) 65 66 const urlParams = new URLSearchParams(window.location.search) 67 const hasOAuthCallback = urlParams.has('code') || urlParams.has('error') 68 69 if (!hasOAuthCallback && hasPendingMigration()) { 70 resumeInfo = getResumeInfo() 71 if (resumeInfo) { 72 const stored = loadMigrationState() 73 if (stored) { 74 if (stored.direction === 'inbound') { 75 direction = 'inbound' 76 inboundFlow = createInboundMigrationFlow() 77 inboundFlow.resumeFromState(stored) 78 } else { 79 direction = 'outbound' 80 outboundFlow = createOutboundMigrationFlow() 81 } 82 } 83 } 84 } 85 86 function selectInbound() { 87 direction = 'inbound' 88 inboundFlow = createInboundMigrationFlow() 89 } 90 91 function selectOutbound() { 92 if (!auth.session) { 93 navigate('/login') 94 return 95 } 96 direction = 'outbound' 97 outboundFlow = createOutboundMigrationFlow() 98 outboundFlow.initLocalClient(auth.session.accessJwt, auth.session.did, auth.session.handle) 99 } 100 101 function handleResume() { 102 const stored = loadMigrationState() 103 if (!stored) return 104 105 showResumeModal = false 106 107 if (stored.direction === 'inbound') { 108 direction = 'inbound' 109 inboundFlow = createInboundMigrationFlow() 110 inboundFlow.resumeFromState(stored) 111 } else { 112 if (!auth.session) { 113 navigate('/login') 114 return 115 } 116 direction = 'outbound' 117 outboundFlow = createOutboundMigrationFlow() 118 outboundFlow.initLocalClient(auth.session.accessJwt, auth.session.did, auth.session.handle) 119 } 120 } 121 122 function handleStartOver() { 123 showResumeModal = false 124 clearMigrationState() 125 resumeInfo = null 126 } 127 128 function handleBack() { 129 if (inboundFlow) { 130 inboundFlow.reset() 131 inboundFlow = null 132 } 133 if (outboundFlow) { 134 outboundFlow.reset() 135 outboundFlow = null 136 } 137 direction = 'select' 138 } 139 140 function handleInboundComplete() { 141 const session = inboundFlow?.getLocalSession() 142 if (session) { 143 setSession({ 144 did: session.did, 145 handle: session.handle, 146 accessJwt: session.accessJwt, 147 refreshJwt: '', 148 }) 149 } 150 navigate('/dashboard') 151 } 152 153 async function handleOutboundComplete() { 154 await logout() 155 navigate('/login') 156 } 157</script> 158 159<div class="migration-page"> 160 {#if showResumeModal && resumeInfo} 161 <div class="modal-overlay"> 162 <div class="modal"> 163 <h2>{$_('migration.resume.title')}</h2> 164 <p>{$_('migration.resume.incomplete')}</p> 165 <div class="resume-details"> 166 <div class="detail-row"> 167 <span class="label">{$_('migration.resume.direction')}:</span> 168 <span class="value">{resumeInfo.direction === 'inbound' ? $_('migration.resume.migratingHere') : $_('migration.resume.migratingAway')}</span> 169 </div> 170 {#if resumeInfo.sourceHandle} 171 <div class="detail-row"> 172 <span class="label">{$_('migration.resume.from')}:</span> 173 <span class="value">{resumeInfo.sourceHandle}</span> 174 </div> 175 {/if} 176 {#if resumeInfo.targetHandle} 177 <div class="detail-row"> 178 <span class="label">{$_('migration.resume.to')}:</span> 179 <span class="value">{resumeInfo.targetHandle}</span> 180 </div> 181 {/if} 182 <div class="detail-row"> 183 <span class="label">{$_('migration.resume.progress')}:</span> 184 <span class="value">{resumeInfo.progressSummary}</span> 185 </div> 186 </div> 187 <p class="note">{$_('migration.resume.reenterCredentials')}</p> 188 <div class="modal-actions"> 189 <button class="ghost" onclick={handleStartOver}>{$_('migration.resume.startOver')}</button> 190 <button onclick={handleResume}>{$_('migration.resume.resumeButton')}</button> 191 </div> 192 </div> 193 </div> 194 {/if} 195 196 {#if oauthLoading} 197 <div class="oauth-loading"> 198 <div class="loading-spinner"></div> 199 <p>{$_('migration.oauthCompleting')}</p> 200 </div> 201 {:else if oauthError} 202 <div class="oauth-error"> 203 <h2>{$_('migration.oauthFailed')}</h2> 204 <p>{oauthError}</p> 205 <button onclick={() => { oauthError = null; direction = 'select' }}>{$_('migration.tryAgain')}</button> 206 </div> 207 {:else if direction === 'select'} 208 <header class="page-header"> 209 <h1>{$_('migration.title')}</h1> 210 <p class="subtitle">{$_('migration.subtitle')}</p> 211 </header> 212 213 <div class="direction-cards"> 214 <button class="direction-card ghost" onclick={selectInbound}> 215 <div class="card-icon"></div> 216 <h2>{$_('migration.migrateHere')}</h2> 217 <p>{$_('migration.migrateHereDesc')}</p> 218 <ul class="features"> 219 <li>{$_('migration.bringDid')}</li> 220 <li>{$_('migration.transferData')}</li> 221 <li>{$_('migration.keepFollowers')}</li> 222 </ul> 223 </button> 224 225 <button class="direction-card ghost" onclick={selectOutbound} disabled> 226 <div class="card-icon"></div> 227 <h2>{$_('migration.migrateAway')}</h2> 228 <p>{$_('migration.migrateAwayDesc')}</p> 229 <ul class="features"> 230 <li>{$_('migration.exportRepo')}</li> 231 <li>{$_('migration.transferToPds')}</li> 232 <li>{$_('migration.updateIdentity')}</li> 233 </ul> 234 <p class="login-required">{$_('migration.comingSoon')}</p> 235 </button> 236 </div> 237 238 <div class="info-section"> 239 <h3>{$_('migration.whatIsMigration')}</h3> 240 <p>{$_('migration.whatIsMigrationDesc')}</p> 241 242 <h3>{$_('migration.beforeMigrate')}</h3> 243 <ul> 244 <li>{$_('migration.beforeMigrate1')}</li> 245 <li>{$_('migration.beforeMigrate2')}</li> 246 <li>{$_('migration.beforeMigrate3')}</li> 247 <li>{$_('migration.beforeMigrate4')}</li> 248 </ul> 249 250 <div class="warning-box"> 251 <strong>Important:</strong> {$_('migration.importantWarning')} 252 <a href="https://github.com/bluesky-social/pds/blob/main/ACCOUNT_MIGRATION.md" target="_blank" rel="noopener"> 253 {$_('migration.learnMore')} 254 </a> 255 </div> 256 </div> 257 258 {:else if direction === 'inbound' && inboundFlow} 259 <InboundWizard 260 flow={inboundFlow} 261 {resumeInfo} 262 onBack={handleBack} 263 onComplete={handleInboundComplete} 264 /> 265 266 {:else if direction === 'outbound' && outboundFlow} 267 <OutboundWizard 268 flow={outboundFlow} 269 onBack={handleBack} 270 onComplete={handleOutboundComplete} 271 /> 272 {/if} 273</div> 274 275<style> 276 .migration-page { 277 max-width: var(--width-lg); 278 margin: var(--space-9) auto; 279 padding: var(--space-7); 280 } 281 282 .page-header { 283 text-align: center; 284 margin-bottom: var(--space-8); 285 } 286 287 .page-header h1 { 288 margin: 0 0 var(--space-3) 0; 289 } 290 291 .subtitle { 292 color: var(--text-secondary); 293 margin: 0; 294 font-size: var(--text-lg); 295 } 296 297 .direction-cards { 298 display: grid; 299 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); 300 gap: var(--space-6); 301 margin-bottom: var(--space-8); 302 } 303 304 .direction-card { 305 background: var(--bg-secondary); 306 border: 1px solid var(--border); 307 border-radius: var(--radius-xl); 308 padding: var(--space-6); 309 text-align: left; 310 cursor: pointer; 311 transition: all 0.2s ease; 312 } 313 314 .direction-card:hover:not(:disabled) { 315 border-color: var(--accent); 316 transform: translateY(-2px); 317 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 318 } 319 320 .direction-card:disabled { 321 opacity: 0.6; 322 cursor: not-allowed; 323 } 324 325 .card-icon { 326 font-size: var(--text-3xl); 327 margin-bottom: var(--space-4); 328 color: var(--accent); 329 } 330 331 .direction-card h2 { 332 margin: 0 0 var(--space-3) 0; 333 font-size: var(--text-xl); 334 color: var(--text-primary); 335 } 336 337 .direction-card p { 338 color: var(--text-secondary); 339 margin: 0 0 var(--space-4) 0; 340 font-size: var(--text-sm); 341 } 342 343 .features { 344 margin: 0; 345 padding-left: var(--space-5); 346 color: var(--text-secondary); 347 font-size: var(--text-sm); 348 } 349 350 .features li { 351 margin-bottom: var(--space-2); 352 } 353 354 .login-required { 355 color: var(--warning-text); 356 font-weight: var(--font-medium); 357 margin-top: var(--space-4); 358 } 359 360 .info-section { 361 background: var(--bg-secondary); 362 border-radius: var(--radius-xl); 363 padding: var(--space-6); 364 } 365 366 .info-section h3 { 367 margin: 0 0 var(--space-3) 0; 368 font-size: var(--text-lg); 369 } 370 371 .info-section h3:not(:first-child) { 372 margin-top: var(--space-6); 373 } 374 375 .info-section p { 376 color: var(--text-secondary); 377 line-height: var(--leading-relaxed); 378 margin: 0; 379 } 380 381 .info-section ul { 382 color: var(--text-secondary); 383 padding-left: var(--space-5); 384 margin: var(--space-3) 0 0 0; 385 } 386 387 .info-section li { 388 margin-bottom: var(--space-2); 389 } 390 391 .warning-box { 392 margin-top: var(--space-6); 393 padding: var(--space-5); 394 background: var(--warning-bg); 395 border: 1px solid var(--warning-border); 396 border-radius: var(--radius-lg); 397 font-size: var(--text-sm); 398 } 399 400 .warning-box strong { 401 color: var(--warning-text); 402 } 403 404 .warning-box a { 405 display: block; 406 margin-top: var(--space-3); 407 color: var(--accent); 408 } 409 410 .modal-overlay { 411 position: fixed; 412 inset: 0; 413 background: rgba(0, 0, 0, 0.5); 414 display: flex; 415 align-items: center; 416 justify-content: center; 417 z-index: 1000; 418 } 419 420 .modal { 421 background: var(--bg-primary); 422 border-radius: var(--radius-xl); 423 padding: var(--space-6); 424 max-width: 400px; 425 width: 90%; 426 } 427 428 .modal h2 { 429 margin: 0 0 var(--space-4) 0; 430 } 431 432 .modal p { 433 color: var(--text-secondary); 434 margin: 0 0 var(--space-4) 0; 435 } 436 437 .resume-details { 438 background: var(--bg-secondary); 439 border-radius: var(--radius-lg); 440 padding: var(--space-4); 441 margin-bottom: var(--space-4); 442 } 443 444 .detail-row { 445 display: flex; 446 justify-content: space-between; 447 padding: var(--space-2) 0; 448 font-size: var(--text-sm); 449 } 450 451 .detail-row:not(:last-child) { 452 border-bottom: 1px solid var(--border); 453 } 454 455 .detail-row .label { 456 color: var(--text-secondary); 457 } 458 459 .detail-row .value { 460 font-weight: var(--font-medium); 461 } 462 463 .note { 464 font-size: var(--text-sm); 465 font-style: italic; 466 } 467 468 .modal-actions { 469 display: flex; 470 gap: var(--space-3); 471 justify-content: flex-end; 472 } 473 474 .oauth-loading { 475 display: flex; 476 flex-direction: column; 477 align-items: center; 478 justify-content: center; 479 padding: var(--space-12); 480 text-align: center; 481 } 482 483 .loading-spinner { 484 width: 48px; 485 height: 48px; 486 border: 3px solid var(--border); 487 border-top-color: var(--accent); 488 border-radius: 50%; 489 animation: spin 1s linear infinite; 490 margin-bottom: var(--space-4); 491 } 492 493 @keyframes spin { 494 to { transform: rotate(360deg); } 495 } 496 497 .oauth-loading p { 498 color: var(--text-secondary); 499 margin: 0; 500 } 501 502 .oauth-error { 503 max-width: 500px; 504 margin: 0 auto; 505 text-align: center; 506 padding: var(--space-8); 507 background: var(--error-bg); 508 border: 1px solid var(--error-border); 509 border-radius: var(--radius-xl); 510 } 511 512 .oauth-error h2 { 513 margin: 0 0 var(--space-4) 0; 514 color: var(--error-text); 515 } 516 517 .oauth-error p { 518 color: var(--text-secondary); 519 margin: 0 0 var(--space-5) 0; 520 } 521</style>