import { authClient } from "@/src/lib/auth-client"; import { apiRequest } from "@/src/lib/api-client"; import { useAuthStore } from "@/src/stores/auth.store"; import { useJoinWithCode } from "@/src/hooks/useInvite"; import { useRouter } from "expo-router"; import { useRef, useState } from "react"; import { ActivityIndicator, KeyboardAvoidingView, Platform, Pressable, Text, TextInput, View, } from "react-native"; import { useTranslation } from "react-i18next"; import { Ionicons } from "@expo/vector-icons"; // Which top-level path the user is on type Mode = "choose" | "create" | "join"; // ── OTP Box Input ───────────────────────────────────────────────────────────── function OtpInput({ onComplete, hasError, }: { onComplete: (code: string) => void; hasError: boolean; }) { const [digits, setDigits] = useState(["", "", "", "", "", ""]); const refs = useRef>([]); function handleChange(text: string, index: number) { // Handle paste: if pasted text fills all 6 slots if (text.length === 6) { const upper = text.toUpperCase(); const filled = upper.split("").slice(0, 6); setDigits(filled); refs.current[5]?.focus(); onComplete(upper); return; } // Handle paste into a single box that is actually 2 chars (current + new char) const char = text.slice(-1).toUpperCase(); const newDigits = [...digits]; newDigits[index] = char; setDigits(newDigits); if (char && index < 5) { refs.current[index + 1]?.focus(); } if (newDigits.every((d) => d !== "")) { onComplete(newDigits.join("")); } } function handleKeyPress(key: string, index: number) { if (key === "Backspace") { const newDigits = [...digits]; if (digits[index] === "" && index > 0) { newDigits[index - 1] = ""; setDigits(newDigits); refs.current[index - 1]?.focus(); } else { newDigits[index] = ""; setDigits(newDigits); } } } return ( {digits.map((digit, i) => ( { refs.current[i] = el; }} value={digit} onChangeText={(text) => handleChange(text, i)} onKeyPress={({ nativeEvent }) => handleKeyPress(nativeEvent.key, i)} autoCapitalize="characters" autoCorrect={false} maxLength={2} selectTextOnFocus style={{ width: 48, height: 56, borderRadius: 12, borderWidth: 1.5, borderColor: hasError ? "#dc2626" : digit ? "#2563EB" : "#e5e7eb", backgroundColor: "#f9fafb", textAlign: "center", fontSize: 24, fontWeight: "700", color: "#111827", }} /> ))} ); } // ── Main Screen ─────────────────────────────────────────────────────────────── export default function OnboardingScreen() { const router = useRouter(); const { t } = useTranslation(); const { setActiveHousehold, setHouseholds, households } = useAuthStore(); const [mode, setMode] = useState("choose"); const [householdName, setHouseholdName] = useState(""); const [isCreating, setIsCreating] = useState(false); const [error, setError] = useState(null); const [pendingCode, setPendingCode] = useState(""); const { mutate: joinWithCode, isPending: isJoining } = useJoinWithCode(); async function handleCreateHousehold() { if (!householdName.trim()) { setError(t('onboarding.enterHouseholdName')); return; } setIsCreating(true); setError(null); try { const result = await authClient.organization.create({ name: householdName.trim(), slug: householdName.trim().toLowerCase().replace(/\s+/g, "-"), }); if (result.error) { setError(result.error.message ?? t('onboarding.createError')); return; } if (result.data?.id) { const organizationId = result.data.id; const newHousehold = { id: organizationId, name: householdName.trim(), role: "owner" }; // Bridge: create households row + seed default categories await apiRequest("/api/households/setup", { method: "POST", headers: { "x-household-id": organizationId }, }); // Append to existing list, keep current active household setHouseholds([...households, newHousehold]); // Only switch if this is the first household (initial onboarding) if (households.length === 0) { setActiveHousehold(organizationId); router.replace("/(auth)/setup"); } else { router.back(); } } } catch { setError(t('onboarding.createError')); } finally { setIsCreating(false); } } function handleCodeComplete(code: string) { setPendingCode(code); } function handleJoinSubmit() { const code = pendingCode.trim().toUpperCase(); if (code.length !== 6) { setError(t('onboarding.enterInviteCode')); return; } setError(null); joinWithCode(code, { onSuccess: async (data) => { const newHousehold = { id: data.householdId, name: data.householdName, role: "member", }; setHouseholds([...households, newHousehold]); setActiveHousehold(data.householdId); router.replace("/(app)/haushalt"); }, onError: (err) => { const msg = err.message ?? t('invite.invalidCode'); if (msg.toLowerCase().includes("already")) { setError(t('invite.alreadyMember')); } else { setError(t('invite.invalidCode')); } }, }); } // ── Choose screen ──────────────────────────────────────────────────────────── if (mode === "choose") { return ( {t('invite.setupTitle')} {t('onboarding.setupSubtitle')} {/* Create new */} { setError(null); setMode("create"); }} className="mb-4 rounded-xl border border-gray-200 bg-white p-5 active:opacity-70" > {t('invite.createNew')} {t('invite.createNewSub')} {/* Join with code */} { setError(null); setMode("join"); }} className="rounded-xl border border-gray-200 bg-white p-5 active:opacity-70" > {t('invite.enterCode')} {t('invite.enterCodeSub')} ); } // ── Create screen ──────────────────────────────────────────────────────────── if (mode === "create") { return ( {/* Back */} { setError(null); setMode("choose"); }} className="mb-6 flex-row items-center gap-1 self-start active:opacity-60" > {t('common.back')} {t('onboarding.setupTitle')} {t('onboarding.setupSubtitle')} {error && ( {error} )} {t('onboarding.householdNameLabel')} {isCreating ? ( ) : ( {t('onboarding.createHousehold')} )} ); } // ── Join screen ────────────────────────────────────────────────────────────── return ( {/* Back */} { setError(null); setPendingCode(""); setMode("choose"); }} className="mb-6 flex-row items-center gap-1 self-start active:opacity-60" > {t('common.back')} {t('invite.joinTitle')} {t('invite.joinHint')} {error && ( {error} )} {isJoining ? ( ) : ( {t('invite.joinButton')} )} ); }