import { AuthScreen } from '@/components/auth/auth-screen'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Text } from '@/components/ui/text'; import { useToast } from '@/components/ui/toast'; import { messageFor } from '@/lib/errors'; import { auth } from '@/lib/firebase'; import { PENDING_EMAIL_KEY } from '@/providers/auth-provider'; import { router } from 'expo-router'; import * as SecureStore from 'expo-secure-store'; import { sendSignInLinkToEmail } from 'firebase/auth'; import { Mail } from 'lucide-react-native'; import { useState } from 'react'; /** * Passwordless sign-in by emailed link. * * Firebase calls this "email link sign-in"; "magic link" is Supabase's word for * the same idea. It is reached only when EXPO_PUBLIC_FIREBASE_LINK_URL is set — * `sign-in.tsx` hides the entry point otherwise — because unlike every other * flow here it cannot work without setup: * * Firebase Dynamic Links shut down on 25 August 2025. The replacement is that * Firebase mints the link on your project's Firebase Hosting domain (or a * custom one passed as `linkDomain`), and your app claims that domain through * Universal Links on iOS and App Links on Android. Both are native * entitlements, so this needs a development or EAS build — it cannot work in * Expo Go, and without the domain claim the link opens a web page that has no * way back into the app. * * `url` must be **https** and its domain must be listed under Authentication → * Settings → Authorized domains. A custom scheme is rejected outright. */ const linkUrl = process.env.EXPO_PUBLIC_FIREBASE_LINK_URL; export default function EmailLinkScreen() { const [email, setEmail] = useState(''); const [sent, setSent] = useState(false); const [loading, setLoading] = useState(false); const toast = useToast(); const send = async () => { if (!linkUrl) return; setLoading(true); const address = email.trim(); try { await sendSignInLinkToEmail(auth, address, { url: linkUrl, // Without this Firebase sends a link that completes in the browser // rather than handing the app an oobCode to spend. handleCodeInApp: true, }); // `signInWithEmailLink` needs the address back, and the link may be // opened from a mail app rather than this one — so it has to be persisted // rather than held in state. The provider reads this key. // // It is deliberately not re-prompted for on the other side: asking "which // address was this?" when a link is opened is a phishing pattern, so a // link opened on a different device is a dead end by design. await SecureStore.setItemAsync(PENDING_EMAIL_KEY, address); setSent(true); } catch (caught) { toast.error('Could not send the link', messageFor(caught)); } finally { setLoading(false); } }; if (sent) { return ( Open it on this device — the link has to come back to the app that requested it. ); } return ( router.back()}> Use a password instead } > email.trim() && send()} /> ); }