Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
11 kB · 325 lines
Svelte
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326<script lang="ts"> import type { AuthMethod, HandlePreservation, ServerDescription, VerificationChannel } from '../../lib/migration/types' import type { VerificationChannel as ApiVerificationChannel } from '../../lib/types/api' import { handleEndsWith } from '../../lib/handle' import { _ } from '../../lib/i18n' import HandleInput from '../HandleInput.svelte' import CommsChannelPicker from '../CommsChannelPicker.svelte'
interface Props { handleInput: string selectedDomain: string email: string password: string authMethod: AuthMethod inviteCode: string serverInfo: ServerDescription | null availableCommsChannels: ApiVerificationChannel[] verificationChannel: VerificationChannel discordUsername: string telegramUsername: string signalUsername: string migratingFromLabel: string migratingFromValue: string loading?: boolean sourceHandle: string sourceDid: string sourcePdsDomains?: string[] handlePreservation: HandlePreservation existingHandleVerified: boolean verifyingExistingHandle?: boolean existingHandleError?: string | null checkAvailability: (fullHandle: string) => Promise<boolean> onHandleChange: (handle: string) => void onDomainChange: (domain: string) => void onEmailChange: (email: string) => void onPasswordChange: (password: string) => void onAuthMethodChange: (method: AuthMethod) => void onInviteCodeChange: (code: string) => void onVerificationChannelChange: (channel: VerificationChannel) => void onDiscordChange: (value: string) => void onTelegramChange: (value: string) => void onSignalChange: (value: string) => void onHandlePreservationChange?: (preservation: HandlePreservation) => void onVerifyExistingHandle?: () => void onBack: () => void onContinue: () => void }
let { handleInput, selectedDomain, email, password, authMethod, inviteCode, serverInfo, availableCommsChannels, verificationChannel, discordUsername, telegramUsername, signalUsername, migratingFromLabel, migratingFromValue, loading = false, sourceHandle, sourceDid, sourcePdsDomains = [], handlePreservation, existingHandleVerified, verifyingExistingHandle = false, existingHandleError = null, checkAvailability, onHandleChange, onDomainChange, onEmailChange, onPasswordChange, onAuthMethodChange, onInviteCodeChange, onVerificationChannelChange, onDiscordChange, onTelegramChange, onSignalChange, onHandlePreservationChange, onVerifyExistingHandle, onBack, onContinue, }: Props = $props()
let handleAvailable = $state<boolean | null>(null) let checkingHandle = $state(false)
const handleTooShort = $derived(handleInput.trim().length > 0 && handleInput.trim().length < 3)
const isSourcePdsManaged = $derived( sourcePdsDomains.length > 0 && sourceHandle.includes('.') && sourcePdsDomains.some(d => handleEndsWith(sourceHandle, d)) )
const isExternalHandle = $derived( sourceHandle.includes('.') && !isSourcePdsManaged && serverInfo != null && !serverInfo.availableUserDomains.some(d => handleEndsWith(sourceHandle, d)) )
const hasVerificationIdentifier = $derived( (verificationChannel === 'email' && email.trim().length > 0) || (verificationChannel === 'discord' && discordUsername.trim().length > 0) || (verificationChannel === 'telegram' && telegramUsername.trim().length > 0) || (verificationChannel === 'signal' && signalUsername.trim().length > 0) )
const canContinue = $derived( hasVerificationIdentifier && (authMethod === 'passkey' || password) && ( (handlePreservation === 'existing' && existingHandleVerified) || (handlePreservation === 'new' && handleInput.trim().length >= 3 && handleAvailable !== false) ) )</script>
<div class="step-content"> <h2>{$_('migration.inbound.chooseHandle.title')}</h2> <p>{$_('migration.inbound.chooseHandle.desc')}</p>
<div class="current-info"> <span class="label">{migratingFromLabel}:</span> <span class="value">{migratingFromValue}</span> </div>
{#if isExternalHandle} <div class="field"> <span class="field-label">{$_('migration.inbound.chooseHandle.handleChoice')}</span> <div class="handle-choice-options"> <label class="handle-choice-option" class:selected={handlePreservation === 'existing'}> <input type="radio" name="handle-preservation" value="existing" checked={handlePreservation === 'existing'} onchange={() => onHandlePreservationChange?.('existing')} /> <div class="handle-choice-content"> <strong>{$_('migration.inbound.chooseHandle.keepExisting')}</strong> <span class="handle-preview">@{sourceHandle}</span> </div> </label> <label class="handle-choice-option" class:selected={handlePreservation === 'new'}> <input type="radio" name="handle-preservation" value="new" checked={handlePreservation === 'new'} onchange={() => onHandlePreservationChange?.('new')} /> <div class="handle-choice-content"> <strong>{$_('migration.inbound.chooseHandle.createNew')}</strong> </div> </label> </div> </div> {/if}
{#if handlePreservation === 'existing' && isExternalHandle} <div class="field"> <span class="field-label">{$_('migration.inbound.chooseHandle.existingHandle')}</span> <div class="existing-handle-display"> <span class="handle-value">@{sourceHandle}</span> {#if existingHandleVerified} <span class="verified-badge">{$_('migration.inbound.chooseHandle.verified')}</span> {/if} </div>
{#if !existingHandleVerified} <div class="verification-instructions"> <p class="instruction-header">{$_('migration.inbound.chooseHandle.verifyInstructions')}</p> <div class="verification-record"> <code>_atproto.{sourceHandle} TXT "did={sourceDid}"</code> </div> <p class="instruction-or">{$_('migration.inbound.chooseHandle.or')}</p> <div class="verification-record"> <code>https://{sourceHandle}/.well-known/atproto-did</code> <span class="record-content">{$_('migration.inbound.chooseHandle.returning')} <code>{sourceDid}</code></span> </div> </div>
<button class="verify-btn" onclick={() => onVerifyExistingHandle?.()} disabled={verifyingExistingHandle} > {#if verifyingExistingHandle} {$_('migration.inbound.chooseHandle.verifying')} {:else if existingHandleError} {$_('migration.inbound.chooseHandle.checkAgain')} {:else} {$_('migration.inbound.chooseHandle.verifyOwnership')} {/if} </button>
{#if existingHandleError} <p class="hint error">{existingHandleError}</p> {/if} {/if} </div> {:else} <div class="field"> <label for="new-handle">{$_('migration.inbound.chooseHandle.newHandle')}</label> <HandleInput id="new-handle" value={handleInput} domains={serverInfo?.availableUserDomains ?? []} {selectedDomain} placeholder="username" {checkAvailability} bind:available={handleAvailable} bind:checking={checkingHandle} onInput={onHandleChange} onDomainChange={onDomainChange} />
{#if handleTooShort} <p class="hint error">{$_('migration.inbound.chooseHandle.handleTooShort')}</p> {:else if checkingHandle} <p class="hint">{$_('migration.inbound.chooseHandle.checkingAvailability')}</p> {:else if handleAvailable === true} <p class="hint" style="color: var(--success-text)">{$_('migration.inbound.chooseHandle.handleAvailable')}</p> {:else if handleAvailable === false} <p class="hint error">{$_('migration.inbound.chooseHandle.handleTaken')}</p> {:else} <p class="hint">{$_('migration.inbound.chooseHandle.handleHint')}</p> {/if} </div> {/if}
<CommsChannelPicker channel={verificationChannel} {email} {discordUsername} {telegramUsername} {signalUsername} availableChannels={availableCommsChannels} disabled={loading} onChannelChange={onVerificationChannelChange} onEmailChange={onEmailChange} onDiscordChange={onDiscordChange} onTelegramChange={onTelegramChange} onSignalChange={onSignalChange} />
<div class="field"> <span class="field-label">{$_('migration.inbound.chooseHandle.authMethod')}</span> <div class="auth-method-options"> <label class="auth-option" class:selected={authMethod === 'password'}> <input type="radio" name="auth-method" value="password" checked={authMethod === 'password'} onchange={() => onAuthMethodChange('password')} /> <div class="auth-option-content"> <strong>{$_('migration.inbound.chooseHandle.authPassword')}</strong> <span>{$_('migration.inbound.chooseHandle.authPasswordDesc')}</span> </div> </label> <label class="auth-option" class:selected={authMethod === 'passkey'}> <input type="radio" name="auth-method" value="passkey" checked={authMethod === 'passkey'} onchange={() => onAuthMethodChange('passkey')} /> <div class="auth-option-content"> <strong>{$_('migration.inbound.chooseHandle.authPasskey')}</strong> <span>{$_('migration.inbound.chooseHandle.authPasskeyDesc')}</span> </div> </label> </div> </div>
{#if authMethod === 'password'} <div class="field"> <label for="new-password">{$_('migration.inbound.chooseHandle.password')}</label> <input id="new-password" type="password" placeholder="Password for your new account" value={password} oninput={(e) => onPasswordChange((e.target as HTMLInputElement).value)} required minlength={8} /> <p class="hint">{$_('migration.inbound.chooseHandle.passwordHint')}</p> </div> {:else} <div class="info-box"> <p>{$_('migration.inbound.chooseHandle.passkeyInfo')}</p> </div> {/if}
{#if serverInfo?.inviteCodeRequired} <div class="field"> <label for="invite">{$_('migration.inbound.chooseHandle.inviteCode')}</label> <input id="invite" type="text" placeholder="Enter invite code" value={inviteCode} oninput={(e) => onInviteCodeChange((e.target as HTMLInputElement).value)} required /> </div> {/if}
<div class="button-row"> <button class="ghost" onclick={onBack} disabled={loading}>{$_('migration.inbound.common.back')}</button> <button disabled={!canContinue || loading} onclick={onContinue}> {$_('migration.inbound.common.continue')} </button> </div></div>