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