import React, {useState} from 'react' import Clipboard from '@react-native-clipboard/clipboard' import * as Toast from '../util/Toast' import { ActivityIndicator, StyleSheet, TouchableOpacity, View, } from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {ScrollView, TextInput} from './util' import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {ErrorMessage} from '../util/error/ErrorMessage' import {useStores} from 'state/index' import {ServiceDescription} from 'state/models/session' import {s} from 'lib/styles' import {makeValidHandle, createFullHandle} from 'lib/strings/handles' import {usePalette} from 'lib/hooks/usePalette' import {useTheme} from 'lib/ThemeContext' import {useAnalytics} from 'lib/analytics' import {cleanError} from 'lib/strings/errors' export const snapPoints = ['100%'] export function Component({onChanged}: {onChanged: () => void}) { const store = useStores() const [error, setError] = useState('') const pal = usePalette('default') const {track} = useAnalytics() const [isProcessing, setProcessing] = useState(false) const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState( {}, ) const [serviceDescription, setServiceDescription] = React.useState< ServiceDescription | undefined >(undefined) const [userDomain, setUserDomain] = React.useState('') const [isCustom, setCustom] = React.useState(false) const [handle, setHandle] = React.useState('') const [canSave, setCanSave] = React.useState(false) // init // = React.useEffect(() => { let aborted = false setError('') setServiceDescription(undefined) setProcessing(true) // load the service description so we can properly provision handles store.session.describeService(String(store.agent.service)).then( desc => { if (aborted) { return } setServiceDescription(desc) setUserDomain(desc.availableUserDomains[0]) setProcessing(false) }, err => { if (aborted) { return } setProcessing(false) store.log.warn( `Failed to fetch service description for ${String( store.agent.service, )}`, err, ) setError( 'Unable to contact your service. Please check your Internet connection.', ) }, ) return () => { aborted = true } }, [store.agent.service, store.session, store.log, retryDescribeTrigger]) // events // = const onPressCancel = React.useCallback(() => { store.shell.closeModal() }, [store]) const onPressRetryConnect = React.useCallback( () => setRetryDescribeTrigger({}), [setRetryDescribeTrigger], ) 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 () => { setError('') setProcessing(true) try { track('EditHandle:SetNewHandle') const newHandle = isCustom ? handle : createFullHandle(handle, userDomain) store.log.debug(`Updating handle to ${newHandle}`) await store.agent.updateHandle({ handle: newHandle, }) store.shell.closeModal() onChanged() } catch (err: any) { setError(cleanError(err)) store.log.error('Failed to update handle', {handle, err}) } finally { setProcessing(false) } }, [ setError, setProcessing, handle, userDomain, store, isCustom, onChanged, track, ]) // rendering // = return ( Cancel Change my handle {isProcessing ? ( ) : error && !serviceDescription ? ( Retry ) : 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() // 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({ handle, canSave, isProcessing, setHandle, onToggleCustom, onPressSave, setCanSave, }: { handle: string canSave: boolean isProcessing: boolean setHandle: (v: string) => void onToggleCustom: () => void onPressSave: () => void setCanSave: (v: boolean) => void }) { const store = useStores() const pal = usePalette('default') const palSecondary = usePalette('secondary') const palError = usePalette('error') const theme = useTheme() const [isVerifying, setIsVerifying] = React.useState(false) const [error, setError] = React.useState('') // events // = const onPressCopy = React.useCallback(() => { Clipboard.setString(`did=${store.me.did}`) Toast.show('Copied to clipboard') }, [store.me.did]) 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 res = await store.agent.com.atproto.identity.resolveHandle({handle}) if (res.data.did === store.me.did) { setCanSave(true) } else { setError(`Incorrect DID returned (got ${res.data.did})`) } } catch (err: any) { setError(cleanError(err)) store.log.error('Failed to verify domain', {handle, err}) } finally { setIsVerifying(false) } }, [ handle, store.me.did, setIsVerifying, setCanSave, setError, canSave, onPressSave, store.log, store.agent, ]) // rendering // = return ( <> Enter the domain you want to use Add the following record to your domain: Domain: _atproto.{handle} Type: TXT Value: did={store.me.did} {canSave === true && ( Domain verified! )} {error && ( {error} )} Nevermind, create a handle for me ) } const styles = StyleSheet.create({ inner: { padding: 14, }, footer: { padding: 14, }, spacer: { height: 20, }, dimmed: { opacity: 0.7, }, 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, }, 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}, })