this repo has no description
1<script lang="ts"> 2 import { onMount } from 'svelte' 3 import { confirmSignup, resendVerification, getAuthState } from '../lib/auth.svelte' 4 import { api, ApiError } from '../lib/api' 5 import { navigate } from '../lib/router.svelte' 6 import { _ } from '../lib/i18n' 7 8 const STORAGE_KEY = 'tranquil_pds_pending_verification' 9 10 interface PendingVerification { 11 did: string 12 handle: string 13 channel: string 14 } 15 16 type VerificationMode = 'signup' | 'token' 17 18 let mode = $state<VerificationMode>('signup') 19 let pendingVerification = $state<PendingVerification | null>(null) 20 let verificationCode = $state('') 21 let identifier = $state('') 22 let submitting = $state(false) 23 let resendingCode = $state(false) 24 let error = $state<string | null>(null) 25 let resendMessage = $state<string | null>(null) 26 let success = $state(false) 27 let autoSubmitting = $state(false) 28 let successPurpose = $state<string | null>(null) 29 let successChannel = $state<string | null>(null) 30 31 const auth = getAuthState() 32 33 34 function parseQueryParams() { 35 const hash = window.location.hash 36 const queryIndex = hash.indexOf('?') 37 if (queryIndex === -1) return {} 38 39 const queryString = hash.slice(queryIndex + 1) 40 const params: Record<string, string> = {} 41 for (const pair of queryString.split('&')) { 42 const [key, value] = pair.split('=') 43 if (key && value) { 44 params[decodeURIComponent(key)] = decodeURIComponent(value) 45 } 46 } 47 return params 48 } 49 50 onMount(async () => { 51 const params = parseQueryParams() 52 53 if (params.token) { 54 mode = 'token' 55 verificationCode = params.token 56 if (params.identifier) { 57 identifier = params.identifier 58 } 59 if (verificationCode && identifier) { 60 autoSubmitting = true 61 await handleTokenVerification() 62 autoSubmitting = false 63 } 64 } else { 65 mode = 'signup' 66 const stored = localStorage.getItem(STORAGE_KEY) 67 if (stored) { 68 try { 69 pendingVerification = JSON.parse(stored) 70 } catch { 71 pendingVerification = null 72 } 73 } 74 } 75 }) 76 77 $effect(() => { 78 if (mode === 'signup' && auth.session) { 79 clearPendingVerification() 80 navigate('/dashboard') 81 } 82 }) 83 84 function clearPendingVerification() { 85 localStorage.removeItem(STORAGE_KEY) 86 pendingVerification = null 87 } 88 89 async function handleSignupVerification(e: Event) { 90 e.preventDefault() 91 if (!pendingVerification || !verificationCode.trim()) return 92 93 submitting = true 94 error = null 95 96 try { 97 await confirmSignup(pendingVerification.did, verificationCode.trim()) 98 clearPendingVerification() 99 navigate('/dashboard') 100 } catch (e: any) { 101 error = e.message || 'Verification failed' 102 } finally { 103 submitting = false 104 } 105 } 106 107 async function handleTokenVerification() { 108 if (!verificationCode.trim() || !identifier.trim()) return 109 110 submitting = true 111 error = null 112 113 try { 114 const result = await api.verifyToken( 115 verificationCode.trim(), 116 identifier.trim(), 117 auth.session?.accessJwt 118 ) 119 success = true 120 successPurpose = result.purpose 121 successChannel = result.channel 122 } catch (e: any) { 123 if (e instanceof ApiError) { 124 if (e.error === 'AuthenticationRequired') { 125 error = 'You must be signed in to complete this verification. Please sign in and try again.' 126 } else { 127 error = e.message 128 } 129 } else { 130 error = 'Verification failed' 131 } 132 } finally { 133 submitting = false 134 } 135 } 136 137 async function handleResendCode() { 138 if (mode === 'signup') { 139 if (!pendingVerification || resendingCode) return 140 141 resendingCode = true 142 resendMessage = null 143 error = null 144 145 try { 146 await resendVerification(pendingVerification.did) 147 resendMessage = $_('verify.codeResent') 148 } catch (e: any) { 149 error = e.message || 'Failed to resend code' 150 } finally { 151 resendingCode = false 152 } 153 } else { 154 if (!identifier.trim() || resendingCode) return 155 156 resendingCode = true 157 resendMessage = null 158 error = null 159 160 try { 161 await api.resendMigrationVerification(identifier.trim()) 162 resendMessage = $_('verify.codeResentDetail') 163 } catch (e: any) { 164 error = e.message || 'Failed to resend verification' 165 } finally { 166 resendingCode = false 167 } 168 } 169 } 170 171 function channelLabel(ch: string): string { 172 switch (ch) { 173 case 'email': return $_('register.email') 174 case 'discord': return $_('register.discord') 175 case 'telegram': return $_('register.telegram') 176 case 'signal': return $_('register.signal') 177 default: return ch 178 } 179 } 180 181 function goToNextStep() { 182 if (successPurpose === 'migration') { 183 navigate('/login') 184 } else if (successChannel === 'email') { 185 navigate('/settings') 186 } else { 187 navigate('/comms') 188 } 189 } 190</script> 191 192<div class="verify-page"> 193 {#if autoSubmitting} 194 <div class="loading-container"> 195 <h1>{$_('verify.verifying')}</h1> 196 <p class="subtitle">{$_('verify.pleaseWait')}</p> 197 </div> 198 {:else if success} 199 <div class="success-container"> 200 <h1>{$_('verify.verified')}</h1> 201 {#if successPurpose === 'migration' || successPurpose === 'signup'} 202 <p class="subtitle">{$_('verify.channelVerified', { values: { channel: channelLabel(successChannel || '') } })}</p> 203 <p class="info-text">{$_('verify.canNowSignIn')}</p> 204 <div class="actions"> 205 <a href="#/login" class="btn">{$_('verify.signIn')}</a> 206 </div> 207 {:else} 208 <p class="subtitle"> 209 {$_('verify.channelVerified', { values: { channel: channelLabel(successChannel || '') } })} 210 </p> 211 <div class="actions"> 212 <button class="btn" onclick={goToNextStep}>{$_('verify.continue')}</button> 213 </div> 214 {/if} 215 </div> 216 {:else if mode === 'token'} 217 <h1>{$_('verify.tokenTitle')}</h1> 218 <p class="subtitle">{$_('verify.tokenSubtitle')}</p> 219 220 {#if error} 221 <div class="message error">{error}</div> 222 {/if} 223 224 {#if resendMessage} 225 <div class="message success">{resendMessage}</div> 226 {/if} 227 228 <form onsubmit={(e) => { e.preventDefault(); handleTokenVerification(); }}> 229 <div class="field"> 230 <label for="identifier">{$_('verify.identifierLabel')}</label> 231 <input 232 id="identifier" 233 type="text" 234 bind:value={identifier} 235 placeholder={$_('verify.identifierPlaceholder')} 236 disabled={submitting} 237 required 238 autocomplete="email" 239 /> 240 <p class="field-help">{$_('verify.identifierHelp')}</p> 241 </div> 242 243 <div class="field"> 244 <label for="verification-code">{$_('verify.codeLabel')}</label> 245 <input 246 id="verification-code" 247 type="text" 248 bind:value={verificationCode} 249 placeholder={$_('verify.codePlaceholder')} 250 disabled={submitting} 251 required 252 autocomplete="off" 253 class="token-input" 254 /> 255 <p class="field-help">{$_('verify.codeHelp')}</p> 256 </div> 257 258 <button type="submit" disabled={submitting || !verificationCode.trim() || !identifier.trim()}> 259 {submitting ? $_('verify.verifying') : $_('verify.verify')} 260 </button> 261 262 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode || !identifier.trim()}> 263 {resendingCode ? $_('verify.sending') : $_('verify.resendCode')} 264 </button> 265 </form> 266 267 <p class="link-text"> 268 <a href="#/login">{$_('verify.backToLogin')}</a> 269 </p> 270 {:else if pendingVerification} 271 <h1>{$_('verify.title')}</h1> 272 <p class="subtitle"> 273 {$_('verify.subtitle', { values: { channel: channelLabel(pendingVerification.channel) } })} 274 </p> 275 <p class="handle-info">{$_('verify.verifyingAccount', { values: { handle: pendingVerification.handle } })}</p> 276 277 {#if error} 278 <div class="message error">{error}</div> 279 {/if} 280 281 {#if resendMessage} 282 <div class="message success">{resendMessage}</div> 283 {/if} 284 285 <form onsubmit={(e) => { e.preventDefault(); handleSignupVerification(e); }}> 286 <div class="field"> 287 <label for="verification-code">{$_('verify.codeLabel')}</label> 288 <input 289 id="verification-code" 290 type="text" 291 bind:value={verificationCode} 292 placeholder={$_('verify.codePlaceholder')} 293 disabled={submitting} 294 required 295 autocomplete="off" 296 class="token-input" 297 /> 298 <p class="field-help">{$_('verify.codeHelp')}</p> 299 </div> 300 301 <button type="submit" disabled={submitting || !verificationCode.trim()}> 302 {submitting ? $_('verify.verifying') : $_('verify.verifyButton')} 303 </button> 304 305 <button type="button" class="secondary" onclick={handleResendCode} disabled={resendingCode}> 306 {resendingCode ? $_('verify.resending') : $_('verify.resendCode')} 307 </button> 308 </form> 309 310 <p class="link-text"> 311 <a href="#/register" onclick={() => clearPendingVerification()}>{$_('verify.startOver')}</a> 312 </p> 313 {:else} 314 <h1>{$_('verify.title')}</h1> 315 <p class="subtitle">{$_('verify.noPending')}</p> 316 <p class="info-text">{$_('verify.noPendingInfo')}</p> 317 318 <div class="actions"> 319 <a href="#/register" class="btn">{$_('verify.createAccount')}</a> 320 <a href="#/login" class="btn secondary">{$_('verify.signIn')}</a> 321 </div> 322 {/if} 323</div> 324 325<style> 326 .verify-page { 327 max-width: var(--width-sm); 328 margin: var(--space-9) auto; 329 padding: var(--space-7); 330 } 331 332 h1 { 333 margin: 0 0 var(--space-3) 0; 334 } 335 336 .subtitle { 337 color: var(--text-secondary); 338 margin: 0 0 var(--space-4) 0; 339 } 340 341 .handle-info { 342 font-size: var(--text-sm); 343 color: var(--text-secondary); 344 margin: 0 0 var(--space-6) 0; 345 } 346 347 .info-text { 348 color: var(--text-secondary); 349 margin: var(--space-4) 0 var(--space-6) 0; 350 } 351 352 form { 353 display: flex; 354 flex-direction: column; 355 gap: var(--space-4); 356 } 357 358 .field-help { 359 font-size: var(--text-xs); 360 color: var(--text-secondary); 361 margin: var(--space-1) 0 0 0; 362 } 363 364 .token-input { 365 font-family: var(--font-mono); 366 letter-spacing: 0.05em; 367 } 368 369 .link-text { 370 text-align: center; 371 margin-top: var(--space-6); 372 font-size: var(--text-sm); 373 } 374 375 .link-text a { 376 color: var(--text-secondary); 377 } 378 379 .actions { 380 display: flex; 381 gap: var(--space-4); 382 } 383 384 .btn { 385 flex: 1; 386 display: inline-block; 387 padding: var(--space-4); 388 background: var(--accent); 389 color: var(--text-inverse); 390 border: none; 391 border-radius: var(--radius-md); 392 font-size: var(--text-base); 393 font-weight: var(--font-medium); 394 cursor: pointer; 395 text-decoration: none; 396 text-align: center; 397 } 398 399 .btn:hover { 400 background: var(--accent-hover); 401 text-decoration: none; 402 } 403 404 .btn.secondary { 405 background: transparent; 406 color: var(--accent); 407 border: 1px solid var(--accent); 408 } 409 410 .btn.secondary:hover { 411 background: var(--accent); 412 color: var(--text-inverse); 413 } 414 415 .success-container, 416 .loading-container { 417 text-align: center; 418 } 419 420 .success-container .actions { 421 justify-content: center; 422 margin-top: var(--space-6); 423 } 424 425 .success-container .btn { 426 flex: none; 427 padding: var(--space-4) var(--space-8); 428 } 429</style>