import {useCallback, useMemo, useState} from 'react' import {useWindowDimensions, View} from 'react-native' import Animated, { FadeIn, FadeOut, LayoutAnimationConfig, LinearTransition, SlideInLeft, SlideInRight, SlideOutLeft, SlideOutRight, } from 'react-native-reanimated' import {ComAtprotoServerDescribeServer} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useMutation, useQueryClient} from '@tanstack/react-query' import {HITSLOP_10} from '#/lib/constants' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {createFullHandle, validateHandle} from '#/lib/strings/handles' import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle' import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {useServiceQuery} from '#/state/queries/service' import {useAgent, useSession} from '#/state/session' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import * as ToggleButton from '#/components/forms/ToggleButton' import { ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon, ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon, } from '#/components/icons/Arrow' import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At' import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {CopyButton} from './CopyButton' export function ChangeHandleDialog({ control, }: { control: Dialog.DialogControlProps }) { const {height} = useWindowDimensions() return ( ) } function ChangeHandleDialogInner() { const control = Dialog.useDialogContext() const {_} = useLingui() const agent = useAgent() const { data: serviceInfo, error: serviceInfoError, refetch, } = useServiceQuery(agent.serviceUrl.toString()) const [page, setPage] = useState<'provided-handle' | 'own-handle'>( 'provided-handle', ) const cancelButton = useCallback( () => ( ), [control, _], ) return ( Change Handle } contentContainerStyle={[a.pt_0, a.px_0]}> {serviceInfoError ? ( ) : serviceInfo ? ( {page === 'provided-handle' ? ( setPage('own-handle')} /> ) : ( setPage('provided-handle')} /> )} ) : ( )} ) } function ProvidedHandlePage({ serviceInfo, goToOwnHandle, }: { serviceInfo: ComAtprotoServerDescribeServer.OutputSchema goToOwnHandle: () => void }) { const {_} = useLingui() const [subdomain, setSubdomain] = useState('') const agent = useAgent() const control = Dialog.useDialogContext() const {currentAccount} = useSession() const queryClient = useQueryClient() const { mutate: changeHandle, isPending, error, isSuccess, } = useUpdateHandleMutation({ onSuccess: () => { if (currentAccount) { queryClient.invalidateQueries({ queryKey: RQKEY_PROFILE(currentAccount.did), }) } agent.resumeSession(agent.session!).then(() => control.close()) }, }) const host = serviceInfo.availableUserDomains[0] const validation = useMemo( () => validateHandle(subdomain, host, true), [subdomain, host], ) const isInvalid = !validation.handleChars || !validation.hyphenStartOrEnd || !validation.totalLength return ( {isSuccess && ( )} {error && ( )} New handle setSubdomain(text)} label={_(msg`New handle`)} placeholder={_(msg`e.g. alice`)} autoCapitalize="none" autoCorrect={false} /> {host} Your full handle will be{' '} @{createFullHandle(subdomain, host)} If you have your own domain, you can use that as your handle. This lets you self-verify your identity.{' '} Learn more here. ) } function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) { const {_} = useLingui() const t = useTheme() const {currentAccount} = useSession() const [dnsPanel, setDNSPanel] = useState(true) const [domain, setDomain] = useState('') const agent = useAgent() const control = Dialog.useDialogContext() const fetchDid = useFetchDid() const queryClient = useQueryClient() const { mutate: changeHandle, isPending, error, isSuccess, } = useUpdateHandleMutation({ onSuccess: () => { if (currentAccount) { queryClient.invalidateQueries({ queryKey: RQKEY_PROFILE(currentAccount.did), }) } agent.resumeSession(agent.session!).then(() => control.close()) }, }) const { mutate: verify, isPending: isVerifyPending, isSuccess: isVerified, error: verifyError, reset: resetVerification, } = useMutation({ mutationKey: ['verify-handle', domain], mutationFn: async () => { const did = await fetchDid(domain) if (did !== currentAccount?.did) { throw new DidMismatchError(did) } return true }, }) return ( {isSuccess && ( )} {error && ( )} {verifyError && ( {verifyError instanceof DidMismatchError ? ( Wrong DID returned from server. Received: {verifyError.did} ) : ( Failed to verify handle. Please try again. )} )} Enter the domain you want to use { setDomain(text) resetVerification() }} autoCapitalize="none" autoCorrect={false} /> setDNSPanel(values[0] === 'dns')}> DNS Panel No DNS Panel {dnsPanel ? ( <> Add the following DNS record to your domain: Host: _atproto Type: TXT Value: did={currentAccount?.did} This should create a domain record at: _atproto.{domain} ) : ( <> Upload a text file to: https://{domain}/.well-known/atproto-did That contains the following: {currentAccount?.did} )} {isVerified && ( )} {currentAccount?.handle?.endsWith('.bsky.social') && ( Your current handle{' '} {sanitizeHandle(currentAccount?.handle || '', '@')} {' '} will automatically remain reserved for you. You can switch back to it at any time from this account. )} ) } class DidMismatchError extends Error { did: string constructor(did: string) { super('DID mismatch') this.name = 'DidMismatchError' this.did = did } } function ChangeHandleError({error}: {error: unknown}) { const {_} = useLingui() let message = _(msg`Failed to change handle. Please try again.`) if (error instanceof Error) { if (error.message.startsWith('Handle already taken')) { message = _(msg`Handle already taken. Please try a different one.`) } else if (error.message === 'Reserved handle') { message = _(msg`This handle is reserved. Please try a different one.`) } else if (error.message === 'Handle too long') { message = _(msg`Handle too long. Please try a shorter one.`) } else if (error.message === 'Input/handle must be a valid handle') { message = _(msg`Invalid handle. Please try a different one.`) } else if (error.message === 'Rate Limit Exceeded') { message = _( msg`Rate limit exceeded – you've tried to change your handle too many times in a short period. Please wait a minute before trying again.`, ) } } return {message} } function SuccessMessage({text}: {text: string}) { const {gtMobile} = useBreakpoints() const t = useTheme() return ( {text} ) }