mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-15 13:56:05 +00:00
325 lines
11 KiB
Svelte
325 lines
11 KiB
Svelte
<script lang="ts">
|
|
import type { AuthMethod, HandlePreservation, ServerDescription, VerificationChannel } from '../../lib/migration/types'
|
|
import type { VerificationChannel as ApiVerificationChannel } from '../../lib/types/api'
|
|
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 => sourceHandle.endsWith(`.${d}`))
|
|
)
|
|
|
|
const isExternalHandle = $derived(
|
|
sourceHandle.includes('.') &&
|
|
!isSourcePdsManaged &&
|
|
serverInfo != null &&
|
|
!serverInfo.availableUserDomains.some(d => sourceHandle.endsWith(`.${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>
|