import React, { useCallback, useEffect, useMemo, useState } from "react"; import { View, Text, ScrollView, StyleSheet, Platform, Alert, TouchableOpacity, } from "react-native"; import { useRouter } from "expo-router"; import * as Linking from "expo-linking"; import { Ionicons } from "@expo/vector-icons"; // Safely import platform auth modules let AppleAuthentication: typeof import("expo-apple-authentication") | null = null; let GoogleSignin: typeof import("@react-native-google-signin/google-signin").GoogleSignin | null = null; if (Platform.OS === "ios") { try { AppleAuthentication = require("expo-apple-authentication"); } catch { AppleAuthentication = null; } } if (Platform.OS === "android") { try { const googleModule = require("@react-native-google-signin/google-signin"); GoogleSignin = googleModule.GoogleSignin; if (GoogleSignin) { GoogleSignin.configure({ webClientId: "764866133492-e78o2rh3rdjc4rfj2j6r2j77b065kggm.apps.googleusercontent.com", }); } } catch { GoogleSignin = null; } } import { GradientBackground, GlassButton, GlassCard, GlassInput } from "../../../components/ui"; import { colors, spacing, radius, typography } from "../../../theme"; import { authClient } from "../../../lib/authClient"; import { apiRequestWithAccessToken } from "../../../lib/apiClient"; import { formatAppleProfileName } from "../../../lib/appleProfileName"; import { persistAppleProfileName } from "../../../lib/appleProfileNamePersistence"; import { getAuthError } from "../../../lib/errors"; import { profileSettingsApi } from "../../../lib/profileSettingsApi"; import { useAuthStore } from "../../../store"; import { ScreenHeader } from "../components"; import type { Profile } from "../../../types"; type SaveMode = "magic-link" | "password"; export function SaveAccountScreen() { const router = useRouter(); const { isAnonymous, fetchUser } = useAuthStore(); const [mode, setMode] = useState("magic-link"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [isChecking, setIsChecking] = useState(false); const [pendingEmail, setPendingEmail] = useState(null); const [pendingPassword, setPendingPassword] = useState(null); const [isAppleAvailable, setIsAppleAvailable] = useState(false); const [isGoogleAvailable, setIsGoogleAvailable] = useState(false); useEffect(() => { if (Platform.OS === "ios" && AppleAuthentication) { AppleAuthentication.isAvailableAsync() .then(setIsAppleAvailable) .catch(() => setIsAppleAvailable(false)); } if (Platform.OS === "android" && GoogleSignin) { setIsGoogleAvailable(true); } }, []); const emailRedirectTo = useMemo(() => Linking.createURL("/(auth)/login"), []); const showError = useCallback((message: string) => { Alert.alert("Error", message); }, []); const showSuccess = useCallback((message: string) => { Alert.alert("Success", message); }, []); const handleSaveWithEmail = useCallback(async () => { if (!email.trim()) { showError("Please enter your email"); return; } if (mode === "password" && password.length < 8) { showError("Password must be at least 8 characters"); return; } setIsSubmitting(true); const { error } = await authClient.auth.updateUser( { email: email.trim() }, { emailRedirectTo } as any ); setIsSubmitting(false); if (error) { const friendly = getAuthError(error); if (__DEV__ && friendly === "Something went wrong. Please try again.") { showError(`${friendly}\n\n${error.message}`); } else { showError(friendly); } return; } setPendingEmail(email.trim()); setPendingPassword(mode === "password" ? password : null); Alert.alert( "Check your email", "We sent you a link to confirm your email. Once you confirm, come back and tap “I’ve verified my email”." ); }, [email, password, mode, emailRedirectTo, showError]); const handleCheckVerification = useCallback(async () => { setIsChecking(true); const { data: { user }, error } = await authClient.auth.getUser(); setIsChecking(false); if (error || !user) { showError(getAuthError(error) || "Unable to check account status"); return; } const isStillAnonymous = !!(user as any).is_anonymous; if (isStillAnonymous) { Alert.alert("Not yet verified", "Please click the link in your email to finish saving your account."); return; } // If user chose password mode, set password after email is verified if (pendingPassword) { setIsSubmitting(true); const { error: passwordError } = await authClient.auth.updateUser({ password: pendingPassword }); setIsSubmitting(false); if (passwordError) { showError(getAuthError(passwordError)); return; } } await fetchUser(); showSuccess("Account saved. You can now recover your account on any device."); router.navigate("/(app)/profile" as any); }, [pendingPassword, fetchUser, showError, showSuccess, router]); const handleLinkApple = useCallback(async () => { if (!AppleAuthentication) return; try { const credential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.FULL_NAME, AppleAuthentication.AppleAuthenticationScope.EMAIL, ], }); if (!credential.identityToken) { showError("No identity token received from Apple"); return; } setIsSubmitting(true); const { error } = await (authClient.auth.linkIdentity as any)({ provider: "apple", token: credential.identityToken, }); setIsSubmitting(false); if (error) { showError(getAuthError(error)); return; } const appleProfileName = formatAppleProfileName(credential.fullName); if (appleProfileName) { try { const { data: { session } } = await authClient.auth.getSession(); if (!session?.user.id || !session.access_token) { throw new Error("Apple account link did not retain an authenticated session"); } await persistAppleProfileName( { userId: session.user.id, accessToken: session.access_token }, appleProfileName, { getProfile: (token) => apiRequestWithAccessToken<{ profile: Profile }>("/v1/me", token), updateProfileName: (token, name) => profileSettingsApi.updateProfileWithAccessToken(token, { name }), }, ); } catch { // Identity linking succeeded. Do not retain the raw Apple name in Auth // metadata if the separate API profile update is temporarily unavailable. console.warn("[Apple Link] Could not persist supplied name"); } } // Refresh user/profile const { data: { session } } = await authClient.auth.getSession(); if (session?.user.id && session.access_token) { await fetchUser({ userId: session.user.id, accessToken: session.access_token, isAnonymous: !!(session.user as { is_anonymous?: boolean }).is_anonymous, }); } else { await fetchUser(); } showSuccess("Account saved with Apple."); router.navigate("/(app)/profile" as any); } catch (e: any) { if (e?.code === "ERR_REQUEST_CANCELED") return; showError("Apple sign in failed. Please try again."); } }, [fetchUser, router, showError, showSuccess]); const handleLinkGoogle = useCallback(async () => { if (!GoogleSignin) return; try { await GoogleSignin.hasPlayServices(); const response = await GoogleSignin.signIn(); if (response.type !== "success" || !response.data.idToken) { return; } setIsSubmitting(true); const { error } = await (authClient.auth.linkIdentity as any)({ provider: "google", token: response.data.idToken, }); setIsSubmitting(false); if (error) { showError(getAuthError(error)); return; } await fetchUser(); showSuccess("Account saved with Google."); router.navigate("/(app)/profile" as any); } catch { showError("Google sign in failed. Please try again."); } }, [fetchUser, router, showError, showSuccess]); const showSocial = Platform.select({ ios: true, android: true, default: false }); if (!isAnonymous) { return ( Your account is already saved You're signed in with a recoverable account. router.navigate("/(app)/profile" as any)}> Back to Settings ); } if (pendingEmail) { return ( Confirm your email We sent a confirmation link to {pendingEmail}. Tap it, then come back here. I've verified my email Resend email { setPendingEmail(null); setPendingPassword(null); }} fullWidth style={{ marginTop: spacing.sm }} > Use a different email ); } return ( Protect your account Unsaved accounts can't be recovered if you delete the app or switch devices. Save your account to keep your couple, matches, and purchases. {showSocial && ( Quick save {Platform.OS === "ios" && ( {isAppleAvailable && AppleAuthentication ? ( ) : ( } fullWidth disabled={isSubmitting} > Save with Apple )} )} {Platform.OS === "android" && isGoogleAvailable && ( } fullWidth disabled={isSubmitting} > Save with Google )} Use an account that isn't already linked to Sauci. )} Save with email setMode("magic-link")} activeOpacity={0.7} > Magic Link setMode("password")} activeOpacity={0.7} > Password } /> {mode === "password" && ( } /> )} Save with Email We'll send a confirmation link to your email. ); } const styles = StyleSheet.create({ container: { flex: 1, }, content: { paddingTop: spacing.lg, paddingBottom: Platform.OS === "ios" ? 40 : 20, gap: spacing.lg, }, heroCard: { marginHorizontal: spacing.lg, }, heroTitle: { ...typography.title3, color: colors.text, marginBottom: spacing.sm, }, heroBody: { ...typography.body, color: colors.textSecondary, lineHeight: 22, }, sectionCard: { marginHorizontal: spacing.lg, }, sectionTitle: { ...typography.headline, color: colors.text, marginBottom: spacing.md, }, sectionHint: { ...typography.caption1, color: colors.textTertiary, marginTop: spacing.md, }, socialRow: { marginBottom: spacing.sm, }, appleButton: { width: "100%", height: 48, }, modeToggleContainer: { flexDirection: "row", backgroundColor: colors.backgroundLight, borderRadius: radius.md, padding: 4, marginBottom: spacing.md, }, modeToggle: { flex: 1, paddingVertical: 12, borderRadius: radius.sm, alignItems: "center", }, modeToggleActive: { backgroundColor: colors.primary, }, modeToggleText: { ...typography.subhead, color: colors.textTertiary, fontWeight: "600", }, modeToggleTextActive: { color: colors.text, }, form: { gap: spacing.sm, }, centered: { flex: 1, justifyContent: "center", alignItems: "center", padding: spacing.lg, }, readyCard: { width: "100%", maxWidth: 420, alignItems: "center", gap: spacing.sm, }, readyTitle: { ...typography.title3, color: colors.text, marginTop: spacing.sm, }, readyBody: { ...typography.body, color: colors.textSecondary, textAlign: "center", marginBottom: spacing.md, }, verifyCard: { width: "100%", maxWidth: 420, alignItems: "center", padding: spacing.lg, }, verifyIcon: { width: 90, height: 90, borderRadius: 45, backgroundColor: colors.primaryLight, justifyContent: "center", alignItems: "center", marginBottom: spacing.md, }, verifyTitle: { ...typography.title3, color: colors.text, marginBottom: spacing.sm, }, verifyBody: { ...typography.body, color: colors.textSecondary, textAlign: "center", lineHeight: 22, marginBottom: spacing.lg, }, }); export default SaveAccountScreen;