import React, {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 {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 {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),
[subdomain, host],
)
const isTooLong = subdomain.length > 18
const isInvalid =
isTooLong ||
!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
.
)
}
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 && (
)}
)
}
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}
)
}