import React, {useState} from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {setStringAsync} from 'expo-clipboard'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {useServiceQuery} from '#/state/queries/service'
import {SessionAccount, useAgent, useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {cleanError} from 'lib/strings/errors'
import {createFullHandle, makeValidHandle} from 'lib/strings/handles'
import {s} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button'
import {SelectableBtn} from '../util/forms/SelectableBtn'
import {Text} from '../util/text/Text'
import * as Toast from '../util/Toast'
import {ScrollView, TextInput} from './util'
export const snapPoints = ['100%']
export type Props = {onChanged: () => void}
export function Component(props: Props) {
const {currentAccount} = useSession()
const agent = useAgent()
const {
isLoading,
data: serviceInfo,
error: serviceInfoError,
} = useServiceQuery(agent.service.toString())
return isLoading || !currentAccount ? (
) : serviceInfoError || !serviceInfo ? (
) : (
)
}
export function Inner({
currentAccount,
serviceInfo,
onChanged,
}: Props & {
currentAccount: SessionAccount
serviceInfo: ComAtprotoServerDescribeServer.OutputSchema
}) {
const {_} = useLingui()
const pal = usePalette('default')
const {track} = useAnalytics()
const {closeModal} = useModalControls()
const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} =
useUpdateHandleMutation()
const agent = useAgent()
const [error, setError] = useState('')
const [isCustom, setCustom] = React.useState(false)
const [handle, setHandle] = React.useState('')
const [canSave, setCanSave] = React.useState(false)
const userDomain = serviceInfo.availableUserDomains?.[0]
// events
// =
const onPressCancel = React.useCallback(() => {
closeModal()
}, [closeModal])
const onToggleCustom = React.useCallback(() => {
// toggle between a provided domain vs a custom one
setHandle('')
setCanSave(false)
setCustom(!isCustom)
track(
isCustom ? 'EditHandle:ViewCustomForm' : 'EditHandle:ViewProvidedForm',
)
}, [setCustom, isCustom, track])
const onPressSave = React.useCallback(async () => {
if (!userDomain) {
logger.error(`ChangeHandle: userDomain is undefined`, {
service: serviceInfo,
})
setError(`The service you've selected has no domains configured.`)
return
}
try {
track('EditHandle:SetNewHandle')
const newHandle = isCustom ? handle : createFullHandle(handle, userDomain)
logger.debug(`Updating handle to ${newHandle}`)
await updateHandle({
handle: newHandle,
})
await agent.resumeSession(agent.session!)
closeModal()
onChanged()
} catch (err: any) {
setError(cleanError(err))
logger.error('Failed to update handle', {handle, message: err})
} finally {
}
}, [
setError,
handle,
userDomain,
isCustom,
onChanged,
track,
closeModal,
updateHandle,
serviceInfo,
agent,
])
// rendering
// =
return (
Cancel
Change Handle
{isUpdateHandlePending ? (
) : canSave ? (
Save
) : undefined}
{error !== '' && (
)}
{isCustom ? (
) : (
)}
)
}
/**
* The form for using a domain allocated by the PDS
*/
function ProvidedHandleForm({
userDomain,
handle,
isProcessing,
setHandle,
onToggleCustom,
setCanSave,
}: {
userDomain: string
handle: string
isProcessing: boolean
setHandle: (v: string) => void
onToggleCustom: () => void
setCanSave: (v: boolean) => void
}) {
const pal = usePalette('default')
const theme = useTheme()
const {_} = useLingui()
// events
// =
const onChangeHandle = React.useCallback(
(v: string) => {
const newHandle = makeValidHandle(v)
setHandle(newHandle)
setCanSave(newHandle.length > 0)
},
[setHandle, setCanSave],
)
// rendering
// =
return (
<>
Your full handle will be{' '}
@{createFullHandle(handle, userDomain)}
I have my own domain
>
)
}
/**
* The form for using a custom domain
*/
function CustomHandleForm({
currentAccount,
handle,
canSave,
isProcessing,
setHandle,
onToggleCustom,
onPressSave,
setCanSave,
}: {
currentAccount: SessionAccount
handle: string
canSave: boolean
isProcessing: boolean
setHandle: (v: string) => void
onToggleCustom: () => void
onPressSave: () => void
setCanSave: (v: boolean) => void
}) {
const pal = usePalette('default')
const palSecondary = usePalette('secondary')
const palError = usePalette('error')
const theme = useTheme()
const {_} = useLingui()
const [isVerifying, setIsVerifying] = React.useState(false)
const [error, setError] = React.useState('')
const [isDNSForm, setDNSForm] = React.useState(true)
const fetchDid = useFetchDid()
// events
// =
const onPressCopy = React.useCallback(() => {
setStringAsync(isDNSForm ? `did=${currentAccount.did}` : currentAccount.did)
Toast.show(_(msg`Copied to clipboard`))
}, [currentAccount, isDNSForm, _])
const onChangeHandle = React.useCallback(
(v: string) => {
setHandle(v)
setCanSave(false)
},
[setHandle, setCanSave],
)
const onPressVerify = React.useCallback(async () => {
if (canSave) {
onPressSave()
}
try {
setIsVerifying(true)
setError('')
const did = await fetchDid(handle)
if (did === currentAccount.did) {
setCanSave(true)
} else {
setError(`Incorrect DID returned (got ${did})`)
}
} catch (err: any) {
setError(cleanError(err))
logger.error('Failed to verify domain', {handle, error: err})
} finally {
setIsVerifying(false)
}
}, [
handle,
currentAccount,
setIsVerifying,
setCanSave,
setError,
canSave,
onPressSave,
fetchDid,
])
// rendering
// =
return (
<>
Enter the domain you want to use
setDNSForm(true)}
accessibilityHint={_(msg`Use the DNS panel`)}
style={s.flex1}
/>
setDNSForm(false)}
accessibilityHint={_(msg`Use a file on your server`)}
style={s.flex1}
/>
{isDNSForm ? (
<>
Add the following DNS record to your domain:
Host:
_atproto
Type:
TXT
Value:
did={currentAccount.did}
This should create a domain record at:
_atproto.{handle}
>
) : (
<>
Upload a text file to:
https://{handle}/.well-known/atproto-did
That contains the following:
{currentAccount.did}
>
)}
{canSave === true && (
Domain verified!
)}
{error ? (
{error}
) : null}
Nevermind, create a handle for me
>
)
}
const styles = StyleSheet.create({
inner: {
padding: 14,
},
footer: {
padding: 14,
},
spacer: {
height: 20,
},
dimmed: {
opacity: 0.7,
},
selectableBtns: {
flexDirection: 'row',
},
title: {
flexDirection: 'row',
alignItems: 'center',
paddingTop: 25,
paddingHorizontal: 20,
paddingBottom: 15,
borderBottomWidth: 1,
},
titleLeft: {
width: 80,
},
titleRight: {
width: 80,
flexDirection: 'row',
justifyContent: 'flex-end',
},
titleMiddle: {
flex: 1,
textAlign: 'center',
fontSize: 21,
},
textInputWrapper: {
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
},
textInputIcon: {
marginLeft: 12,
},
textInput: {
flex: 1,
width: '100%',
paddingVertical: 10,
paddingHorizontal: 8,
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '400',
borderRadius: 10,
},
valueContainer: {
borderRadius: 4,
paddingVertical: 16,
},
dnsTable: {
borderRadius: 4,
paddingTop: 2,
paddingBottom: 16,
},
dnsLabel: {
paddingHorizontal: 14,
paddingTop: 10,
},
dnsValue: {
paddingHorizontal: 14,
borderRadius: 4,
},
monoText: {
fontSize: 18,
lineHeight: 20,
},
message: {
paddingHorizontal: 12,
paddingVertical: 10,
borderRadius: 8,
marginBottom: 10,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
borderRadius: 32,
padding: 10,
marginBottom: 10,
},
errorContainer: {marginBottom: 10},
})