diff --git a/apps/amethyst/app/auth/login.tsx b/apps/amethyst/app/auth/login.tsx index 41c7d23..abcf3a3 100644 --- a/apps/amethyst/app/auth/login.tsx +++ b/apps/amethyst/app/auth/login.tsx @@ -1,43 +1,188 @@ import { Link, Stack, router } from "expo-router"; import { AlertCircle, AtSign, Check, ChevronRight } from "lucide-react-native"; -import React, { useState } from "react"; -import { Platform, View } from "react-native"; +import React, { useState, useEffect, useCallback, useRef } from "react"; // Added useCallback, useRef +import { Platform, TextInput, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Text } from "@/components/ui/text"; import { Icon } from "@/lib/icons/iconWithClassName"; -import { cn } from "@/lib/utils"; +import { capFirstLetter, cn } from "@/lib/utils"; import { openAuthSessionAsync } from "expo-web-browser"; import { useStore } from "@/stores/mainStore"; +import { resolveFromIdentity } from "@/lib/atp/pid"; + +import Animated, { + useSharedValue, + useAnimatedStyle, + withTiming, + interpolate, +} from "react-native-reanimated"; +import { MaterialCommunityIcons, FontAwesome6 } from "@expo/vector-icons"; + +type Url = URL; + +interface ResolvedIdentity { + pds: Url; + [key: string]: any; +} + +const DEBOUNCE_DELAY = 500; // 500ms debounce delay const LoginScreen = () => { const [handle, setHandle] = useState(""); const [err, setErr] = useState(); const [isRedirecting, setIsRedirecting] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [isSelected, setIsSelected] = useState(false); + + const [pdsUrl, setPdsUrl] = useState(null); + const [isResolvingPds, setIsResolvingPds] = useState(false); + const [pdsResolutionError, setPdsResolutionError] = useState< + string | undefined + >(); + + const handleInputRef = useRef(null); const { getLoginUrl, oauthCallback } = useStore((state) => state); + const messageAnimation = useSharedValue(0); + + // focus on load + useEffect(() => { + if (handleInputRef.current) { + handleInputRef.current.focus(); + } + }, []); + + useEffect(() => { + if (isResolvingPds || pdsResolutionError || pdsUrl) { + messageAnimation.value = withTiming(1, { duration: 500 }); + } else { + messageAnimation.value = withTiming(0, { duration: 400 }); + } + }, [isResolvingPds, pdsResolutionError, messageAnimation, pdsUrl]); + + const messageContainerAnimatedStyle = useAnimatedStyle(() => { + return { + opacity: messageAnimation.value, + maxHeight: interpolate(messageAnimation.value, [0, 1], [0, 100]), + marginTop: -8, + paddingTop: 8, + overflow: "hidden", + zIndex: -1, + }; + }); + + const getPdsUrl = useCallback( + async ( + currentHandle: string, + callbacks?: { + onSuccess?: (resolvedPdsUrl: Url) => void; + onError?: (errorMessage: string) => void; + }, + ): Promise => { + // Ensure we're not calling with an empty or whitespace-only handle due to debounce race. + if (!currentHandle || currentHandle.trim() === "") { + // Clear any potential resolving/error states if triggered by empty text + setPdsResolutionError(undefined); + setIsResolvingPds(false); + setPdsUrl(null); + callbacks?.onError?.("Handle cannot be empty for PDS resolution."); // Optional: notify caller + return; + } + + setIsResolvingPds(true); + setPdsResolutionError(undefined); + setPdsUrl(null); + try { + console.log(`Attempting to resolve PDS for handle: ${currentHandle}`); + const identity: ResolvedIdentity | null = + await resolveFromIdentity(currentHandle); + + if (!identity || !identity.pds) { + throw new Error("Could not resolve PDS from the provided handle."); + } + + setPdsUrl(identity.pds); + callbacks?.onSuccess?.(identity.pds); + setIsResolvingPds(false); + } catch (e: any) { + const errorMessage = + e.message || "An unknown error occurred while resolving PDS."; + setPdsResolutionError(errorMessage); + callbacks?.onError?.(errorMessage); + } finally { + if (pdsResolutionError && isResolvingPds) { + setIsResolvingPds(false); + } + } + }, + [isResolvingPds, pdsResolutionError], + ); + const debounceTimeoutRef = useRef(null); + + useEffect(() => { + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + }; + }, []); + + const handleTextChange = useCallback( + (text: string) => { + setHandle(text); + + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + + if (text.trim().length > 3) { + debounceTimeoutRef.current = setTimeout(() => { + getPdsUrl(text.trim(), { + onSuccess: (u) => { + setPdsUrl(u); + }, + onError: (e) => { + console.error(e); + setPdsResolutionError("Couldn't resolve handle"); + }, + }); + }, DEBOUNCE_DELAY); + } else { + setPdsResolutionError(undefined); + setIsResolvingPds(false); + setPdsUrl(null); + } + }, + [getPdsUrl], + ); + const handleLogin = async () => { + // reset state + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + setIsResolvingPds(false); + setPdsResolutionError(undefined); + if (!handle) { setErr("Please enter a handle"); return; } setIsLoading(true); + setErr(undefined); try { let redirUrl = await getLoginUrl(handle.replace("@", "")); if (!redirUrl) { - // TODO: better error handling lulw - throw new Error("Could not get login url. "); + throw new Error("Could not get login url."); } setIsRedirecting(true); if (Platform.OS === "web") { - // redirect to redir url page without authsession - // shyould! redirect to /auth/callback router.navigate(redirUrl.toString()); } else { const res = await openAuthSessionAsync( @@ -47,11 +192,17 @@ const LoginScreen = () => { if (res.type === "success") { const params = new URLSearchParams(res.url.split("?")[1]); await oauthCallback(params); + } else if (res.type === "cancel" || res.type === "dismiss") { + setErr("Login cancelled by user."); + setIsRedirecting(false); + setIsLoading(false); + return; + } else { + throw new Error("Authentication failed or was cancelled."); } } } catch (e: any) { - console.error(e); - setErr(e.message); + setErr(e.message || "An unknown error occurred during login."); setIsLoading(false); setIsRedirecting(false); return; @@ -67,7 +218,7 @@ const LoginScreen = () => { headerShown: false, }} /> - + @@ -77,10 +228,16 @@ const LoginScreen = () => { Handle setIsSelected(true)} + onBlur={(e) => setIsSelected(false)} autoCapitalize="none" autoCorrect={false} onKeyPress={(e) => { @@ -89,24 +246,66 @@ const LoginScreen = () => { } }} /> - {err ? ( - - - {err} - - ) : ( - - )} + + + + {pdsUrl !== null ? ( + + PDS:{" "} + {pdsUrl.hostname.includes("bsky.network") && ( + + + + + )} + {pdsUrl.hostname.includes("bsky.network") + ? capFirstLetter(pdsUrl.hostname.split(".").shift() || "") + : pdsUrl.hostname} + + ) : pdsResolutionError ? ( + + + {pdsResolutionError} + + ) : ( + + Resolving PDS... + + )} + + - - - Sign up for Bluesky - + +