From 19c3bb6ce8b26a4dd497fb66af5447348411d943 Mon Sep 17 00:00:00 2001 From: uwx Date: Mon, 8 Dec 2025 23:48:32 +0100 Subject: [PATCH] feat: add pds auto resolving from catsky --- src/components/dialogs/ServerInput.tsx | 150 +++++++++-------------- src/components/forms/HostingProvider.tsx | 14 +-- src/screens/Login/LoginForm.tsx | 29 ++++- src/screens/Login/index.tsx | 64 ++++++++-- 4 files changed, 148 insertions(+), 109 deletions(-) diff --git a/src/components/dialogs/ServerInput.tsx b/src/components/dialogs/ServerInput.tsx index d7c02bb9f..cf52fad9c 100644 --- a/src/components/dialogs/ServerInput.tsx +++ b/src/components/dialogs/ServerInput.tsx @@ -31,8 +31,6 @@ export function ServerInputDialog({ const formRef = useRef(null) // persist these options between dialog open/close - const [fixedOption, setFixedOption] = - useState(BSKY_SERVICE) const [previousCustomAddress, setPreviousCustomAddress] = useState('') const onClose = useCallback(() => { @@ -44,9 +42,9 @@ export function ServerInputDialog({ } } logger.metric('signin:hostingProviderPressed', { - hostingProviderDidChange: fixedOption !== BSKY_SERVICE, + hostingProviderDidChange: false, // stubbed for PDS auto-resolution }) - }, [onSelect, fixedOption]) + }, [onSelect]) return ( @@ -71,13 +67,9 @@ type DialogInnerRef = {getFormState: () => string | null} function DialogInner({ formRef, - fixedOption, - setFixedOption, initialCustomAddress, }: { formRef: React.Ref - fixedOption: SegmentedControlOptions - setFixedOption: (opt: SegmentedControlOptions) => void initialCustomAddress: string }) { const control = Dialog.useDialogContext() @@ -94,14 +86,9 @@ function DialogInner({ formRef, () => ({ getFormState: () => { - let url - if (fixedOption === 'custom') { - url = customAddress.trim().toLowerCase() - if (!url) { - return null - } - } else { - url = fixedOption + let url = customAddress.trim().toLowerCase() + if (!url) { + return null } if (!url.startsWith('http://') && !url.startsWith('https://')) { if (url === 'localhost' || url.startsWith('localhost:')) { @@ -111,18 +98,16 @@ function DialogInner({ } } - if (fixedOption === 'custom') { - if (!pdsAddressHistory.includes(url)) { - const newHistory = [url, ...pdsAddressHistory.slice(0, 4)] - setPdsAddressHistory(newHistory) - persisted.write('pdsAddressHistory', newHistory) - } + if (!pdsAddressHistory.includes(url)) { + const newHistory = [url, ...pdsAddressHistory.slice(0, 4)] + setPdsAddressHistory(newHistory) + persisted.write('pdsAddressHistory', newHistory) } return url }, }), - [customAddress, fixedOption, pdsAddressHistory], + [customAddress, pdsAddressHistory], ) const isFirstTimeUser = accounts.length === 0 @@ -136,76 +121,57 @@ function DialogInner({ Choose your account provider - - - - {_(msg`Bluesky`)} - - - - - {_(msg`Custom`)} - - - - - {fixedOption === BSKY_SERVICE && isFirstTimeUser && ( - - - - Bluesky is an open network where you can choose your own - provider. If you're new here, we recommend sticking with the - default Bluesky Social option. - - - - )} - {fixedOption === 'custom' && ( - - - Server address - - - - - - {pdsAddressHistory.length > 0 && ( - - {pdsAddressHistory.map(uri => ( - - ))} - - )} - + {isFirstTimeUser && ( + + + Bluesky is an open network where you can choose your own provider. + If you're new here, we recommend sticking with the default Bluesky + Social option. + + )} + + + Server address + + + + + + {pdsAddressHistory.length > 0 && ( + + {pdsAddressHistory.map(uri => ( + + ))} + + )} + + diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx index b7d23ba3a..f6b52e4a4 100644 --- a/src/components/forms/HostingProvider.tsx +++ b/src/components/forms/HostingProvider.tsx @@ -18,7 +18,7 @@ export function HostingProvider({ onOpenDialog, minimal, }: { - serviceUrl: string + serviceUrl?: string | undefined onSelectServiceUrl: (provider: string) => void onOpenDialog?: () => void minimal?: boolean @@ -26,6 +26,8 @@ export function HostingProvider({ const serverInputControl = useDialogControl() const t = useTheme() const {_} = useLingui() + const serviceProviderLabel = + serviceUrl === undefined ? _(msg`Automatic`) : toNiceDomain(serviceUrl) const onPressSelectService = React.useCallback(() => { Keyboard.dismiss() @@ -45,7 +47,7 @@ export function HostingProvider({ You are creating an account on ) : ( - ) : !serviceDescription ? ( + ) : !serviceDescription && serviceUrl !== undefined ? ( <> diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 9cbbd5121..e4ecdb9d4 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -1,14 +1,16 @@ -import {useEffect, useRef, useState} from 'react' +import {useCallback,useEffect, useMemo, useRef, useState} from 'react' import {KeyboardAvoidingView} from 'react-native' import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import debounce from 'lodash.debounce' import {DEFAULT_SERVICE} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' +import {resolvePdsServiceUrl} from '#/state/queries/resolve-identity' import {useServiceQuery} from '#/state/queries/service' -import {type SessionAccount, useSession} from '#/state/session' +import {type SessionAccount, useAgent, useSession} from '#/state/session' import {useLoggedOutView} from '#/state/shell/logged-out' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' @@ -18,6 +20,7 @@ import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' import {atoms as a, native} from '#/alf' import {ScreenTransition} from '#/components/ScreenTransition' import {ChooseAccountForm} from './ChooseAccountForm' +import { Did } from '@atproto/api' enum Forms { Login, @@ -40,15 +43,17 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { const failedAttemptCountRef = useRef(0) const startTimeRef = useRef(Date.now()) + const agent = useAgent() const {accounts} = useSession() const {requestedAccountSwitchTo} = useLoggedOutView() const requestedAccount = accounts.find( acc => acc.did === requestedAccountSwitchTo, ) - const [error, setError] = useState('') - const [serviceUrl, setServiceUrl] = useState( - requestedAccount?.service || DEFAULT_SERVICE, + const [isResolvingService, setIsResolvingService] = useState(false) + const [error, setError] = useState('') + const [serviceUrl, setServiceUrl] = useState( + requestedAccount?.service, ) const [initialHandle, setInitialHandle] = useState( requestedAccount?.handle || '', @@ -68,7 +73,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { data: serviceDescription, error: serviceError, refetch: refetchService, - } = useServiceQuery(serviceUrl) + } = useServiceQuery(serviceUrl ?? '') const onSelectAccount = (account?: SessionAccount) => { if (account?.service) { @@ -102,6 +107,47 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } }, [serviceError, serviceUrl, _]) + const resolveIdentity = useCallback( + async (identifier: string) => { + setIsResolvingService(true) + + try { + const getDid = async () => { + if (identifier.startsWith('did:')) return identifier + else + return ( + await agent.resolveHandle({ + handle: identifier, + }) + ).data.did + } + + const did = (await getDid()) as Did + const pdsUrl = await resolvePdsServiceUrl(did) + + if (!pdsUrl) { + throw new Error(`No PDS service found in DID document for ${did}`) + } + + if (pdsUrl.endsWith('.bsky.network')) { + setServiceUrl('https://bsky.social') + } else { + setServiceUrl(pdsUrl) + } + } catch (err) { + logger.error(`Service auto-resolution failed: ${err}`) + } finally { + setIsResolvingService(false) + } + }, + [agent], + ) + + const debouncedResolveService = useMemo( + () => debounce(resolveIdentity, 800), + [resolveIdentity], + ) + const onPressForgotPassword = () => { gotoForm(Forms.ForgotPassword) logEvent('signin:forgotPasswordPressed', {}) @@ -150,6 +196,8 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } onPressForgotPassword={onPressForgotPassword} onPressRetryConnect={refetchService} + debouncedResolveService={debouncedResolveService} + isResolvingService={isResolvingService} /> ) break @@ -169,7 +217,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { content = ( void}) => { content = ( gotoForm(Forms.ForgotPassword)} onPasswordSet={() => gotoForm(Forms.PasswordUpdated)} -- 2.46.2.windows.1