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}, })