Amba-Agent-App/app/(root)/auth/phone-setup.tsx
2026-01-16 00:22:35 +03:00

466 lines
15 KiB
TypeScript

import React, { useState, useEffect, useRef } from "react";
import {
View,
ScrollView,
Keyboard,
Platform,
KeyboardAvoidingView,
TouchableWithoutFeedback,
Image,
TouchableOpacity,
Alert,
} from "react-native";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { Text } from "~/components/ui/text";
import { router } from "expo-router";
import { ROUTES } from "~/lib/routes";
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
import { AuthService } from "~/lib/services/authServices";
import BackButton from "~/components/ui/backButton";
import WalletService from "~/lib/services/walletService";
import {
fullNameSchema,
emailSchema,
pinSchema,
confirmPinSchema,
addressSchema,
validate,
} from "~/lib/utils/validationSchemas";
import { FCMService } from "~/lib/services/fcmService";
import { useAuthStore } from "~/lib/stores/authStore";
import ScreenWrapper from "~/components/ui/ScreenWrapper";
import ModalToast from "~/components/ui/toast";
import { useTranslation } from "react-i18next";
import * as ImagePicker from "expo-image-picker";
import { Icons } from "~/assets/icons";
import { Plus } from "lucide-react-native";
import { uploadProfileImage } from "~/lib/services/profileImageService";
export default function PhoneSetup() {
const [fullName, setFullName] = useState("");
const [email, setEmail] = useState("");
const [address, setAddress] = useState("");
const [pin, setPin] = useState("");
const [confirmPin, setConfirmPin] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { t } = useTranslation();
const { user, clearPhoneAuth, refreshProfile, refreshWallet } =
useAuthWithProfile();
const { signOut } = useAuthStore();
const [profileImage, setProfileImage] = useState<string | null>(null);
console.log("PHONE SETUP PAGE LOADED, user:", user?.uid);
const [toastVisible, setToastVisible] = useState(false);
const [toastTitle, setToastTitle] = useState("");
const [toastDescription, setToastDescription] = useState<string | undefined>(
undefined
);
const [toastVariant, setToastVariant] = useState<
"success" | "error" | "warning" | "info"
>("info");
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const showToast = (
title: string,
description?: string,
variant: "success" | "error" | "warning" | "info" = "info"
) => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
setToastTitle(title);
setToastDescription(description);
setToastVariant(variant);
setToastVisible(true);
toastTimeoutRef.current = setTimeout(() => {
setToastVisible(false);
toastTimeoutRef.current = null;
}, 2500);
};
// Redirect if no user is authenticated
useEffect(() => {
// In dev, allow the fake emulator user through without redirecting
if (__DEV__ && user?.uid === "dev-emulator-user") {
return;
}
if (!user) {
console.log("NO USER FOUND, redirecting to signin");
router.replace(ROUTES.SIGNIN);
}
}, [user]);
const handleSelectProfileImage = async () => {
try {
const permissionResult =
await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permissionResult.granted) {
Alert.alert(
"Permission Required",
"Please allow access to your photo library to select a profile picture."
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 0.8,
});
if (!result.canceled && result.assets[0]) {
setProfileImage(result.assets[0].uri);
}
} catch (e) {
showToast("Error", "Failed to select image", "error");
}
};
const handleCompleteSetup = async () => {
Keyboard.dismiss();
// Dev-only shortcut: on emulator/dev builds, skip profile setup and go straight home
if (__DEV__) {
console.log(
"DEV phone setup bypass: skipping profile creation and navigating to HOME"
);
router.replace(ROUTES.HOME);
return;
}
// Validate full name
const fullNameResult = validate(fullNameSchema, fullName);
if (!fullNameResult.success) {
showToast("Error", fullNameResult.error, "warning");
return;
}
// Validate email
const emailResult = validate(emailSchema, email);
if (!emailResult.success) {
showToast("Error", emailResult.error, "warning");
return;
}
// Validate PIN
const pinResult = validate(pinSchema, pin);
if (!pinResult.success) {
showToast("Error", pinResult.error, "warning");
return;
}
// Validate confirm PIN
const confirmPinResult = validate(confirmPinSchema(pin), confirmPin);
if (!confirmPinResult.success) {
showToast("Error", confirmPinResult.error, "warning");
return;
}
// Validate address (optional)
const addressResult = validate(addressSchema, address);
if (!addressResult.success) {
showToast("Error", addressResult.error, "warning");
return;
}
if (!user) {
showToast(
t("phoneSetup.toastNoUserTitle"),
t("phoneSetup.toastNoUser"),
"error"
);
return;
}
setLoading(true);
setError(null);
try {
let photoUrl: string | undefined;
if (profileImage) {
try {
photoUrl = await uploadProfileImage(user.uid, profileImage);
} catch (e) {
console.warn("Failed to upload profile image during setup", e);
}
}
await AuthService.createUserProfile(user.uid, {
fullName: fullName.trim(),
phoneNumber: user.phoneNumber || undefined,
address: address.trim() || undefined,
email: email.trim(),
pin: pin.trim(),
signupType: "phone", // Track that this user signed up with phone
createdAt: new Date(),
updatedAt: new Date(),
photoUrl,
});
// Initialize FCM token for new user (Android only)
if (Platform.OS === "android") {
try {
const fcmResult = await FCMService.initializeTokenForNewUser(
user.uid
);
if (fcmResult.success) {
console.log("FCM token initialized for new user");
} else {
console.warn("Failed to initialize FCM token:", fcmResult.error);
// Don't fail the setup if FCM fails
}
} catch (fcmError) {
console.error("Error initializing FCM token:", fcmError);
// Don't fail the setup if FCM fails
}
}
// Create wallet for the user
await WalletService.createUserWallet(user.uid);
console.log("User wallet created successfully");
// Clear phone auth state
clearPhoneAuth();
// Refresh profile and wallet to update auth context
await refreshProfile();
await refreshWallet();
router.replace(ROUTES.HOME);
} catch (error) {
console.error("Profile setup error:", error);
setError(
error instanceof Error ? error.message : "Failed to complete setup"
);
showToast(
t("phoneSetup.validationErrorTitle"),
t("phoneSetup.toastSetupError"),
"error"
);
} finally {
setLoading(false);
}
};
// Show errors
useEffect(() => {
if (error) {
showToast(t("phoneSetup.toastSetupErrorTitle"), error, "error");
}
}, [error]);
useEffect(() => {
return () => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
if (!user) {
return null; // Redirect handled in effect above
}
const handleBackPress = () => {
showToast("Cancel Setup", "Are you sure you want to cancel?", "warning");
// Give user time to see the toast, then handle navigation
setTimeout(async () => {
try {
clearPhoneAuth();
await signOut();
router.replace(ROUTES.SIGNIN);
} catch (error) {
console.error("Error signing out:", error);
router.replace(ROUTES.SIGNIN);
}
}, 1500);
};
return (
<ScreenWrapper edges={[]}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
className="flex-1"
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View className="flex-1">
<BackButton onPress={handleBackPress} />
<ScrollView
className="flex px-5 space-y-3 w-full"
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View className="flex flex-col space-y-1 py-5 items-left">
<Text className="text-xl font-dmsans text-primary">
{t("phoneSetup.title")}
</Text>
<View className="h-4" />
<Text className="text-base font-dmsans text-gray-400">
{t("phoneSetup.subtitle")}
</Text>
</View>
<View className="items-center mt-6 mb-4">
<TouchableOpacity
onPress={handleSelectProfileImage}
activeOpacity={0.8}
className="mb-4 relative"
>
<View className="w-24 h-24 rounded-full bg-[#C8E6C9] items-center justify-center overflow-hidden">
{profileImage ? (
<Image
source={{ uri: profileImage }}
className="w-24 h-24 rounded-full"
resizeMode="cover"
/>
) : (
<Image
source={Icons.avatar}
style={{ width: 84, height: 84, resizeMode: "contain" }}
/>
)}
</View>
<View className="absolute bottom-0 -right-2 w-8 h-8 rounded-full bg-primary items-center justify-center border-2 border-white">
<Plus size={16} color="#fff" strokeWidth={3} />
</View>
</TouchableOpacity>
</View>
<View className="h-6" />
<Label className="text-base font-dmsans-medium">
{t("phoneSetup.fullNameLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("phoneSetup.fullNamePlaceholder")}
value={fullName}
onChangeText={setFullName}
containerClassName="w-full"
borderClassName="border-0 bg-transparent"
placeholderColor="#9CA3AF"
placeholderClassName="text-xs"
textClassName="text-[#000] text-sm"
/>
<View className="h-6" />
<Label className="text-base font-dmsans-medium">
{t("phoneSetup.addressLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("phoneSetup.addressPlaceholder")}
value={address}
onChangeText={setAddress}
containerClassName="w-full"
borderClassName="border-0 bg-transparent"
placeholderColor="#9CA3AF"
placeholderClassName="text-xs"
textClassName="text-[#000] text-sm"
/>
<View className="h-6" />
<Label className="text-base font-dmsans-medium">
{t("phoneSetup.emailLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("phoneSetup.emailPlaceholder")}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
containerClassName="w-full"
borderClassName="border-0 bg-transparent"
placeholderColor="#9CA3AF"
placeholderClassName="text-xs"
textClassName="text-[#000] text-sm"
/>
<View className="h-6" />
<Label className="text-base font-dmsans-medium">
{t("phoneSetup.pinLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("phoneSetup.pinPlaceholder")}
value={pin}
onChangeText={setPin}
keyboardType="numeric"
secureTextEntry={true}
maxLength={6}
containerClassName="w-full"
borderClassName="border-0 bg-transparent"
placeholderColor="#9CA3AF"
placeholderClassName="text-xs"
textClassName="text-[#000] text-sm"
/>
<View className="h-6" />
<Label className="text-base font-dmsans-medium">
{t("phoneSetup.confirmPinLabel")}
</Label>
<View className="h-2" />
<Input
placeholder={t("phoneSetup.confirmPinPlaceholder")}
value={confirmPin}
onChangeText={setConfirmPin}
keyboardType="numeric"
secureTextEntry={true}
maxLength={6}
containerClassName="w-full"
borderClassName="border-0 bg-transparent"
placeholderColor="#9CA3AF"
placeholderClassName="text-xs"
textClassName="text-[#000] text-sm"
/>
<View className="h-8" />
<Button
className="bg-primary rounded-[8px]"
onPress={handleCompleteSetup}
disabled={
loading ||
!fullName.trim() ||
!email.trim() ||
!pin.trim() ||
!confirmPin.trim()
}
>
<Text className="font-dmsans text-white">
{loading
? t("phoneSetup.buttonLoading")
: t("phoneSetup.button")}
</Text>
</Button>
<View className="h-8" />
</ScrollView>
</View>
</TouchableWithoutFeedback>
</KeyboardAvoidingView>
<ModalToast
visible={toastVisible}
title={toastTitle}
description={toastDescription}
variant={toastVariant}
/>
</ScreenWrapper>
);
}