466 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|