451 lines
14 KiB
TypeScript
451 lines
14 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import {
|
|
View,
|
|
ScrollView,
|
|
Keyboard,
|
|
Platform,
|
|
KeyboardAvoidingView,
|
|
TouchableWithoutFeedback,
|
|
Image,
|
|
TouchableOpacity,
|
|
} 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 ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import WalletService from "~/lib/services/walletService";
|
|
import {
|
|
fullNameSchema,
|
|
phoneNumberSchema,
|
|
pinSchema,
|
|
confirmPinSchema,
|
|
addressSchema,
|
|
validate,
|
|
} from "~/lib/utils/validationSchemas";
|
|
import { useAuthStore } from "~/lib/stores/authStore";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import * as ImagePicker from "expo-image-picker";
|
|
import { Icons } from "~/assets/icons";
|
|
import { Plus } from "lucide-react-native";
|
|
import { uploadProfileImage } from "~/lib/services/profileImageService";
|
|
|
|
// Conditionally import FCMService only for native
|
|
let FCMService: any = null;
|
|
if (Platform.OS !== "web") {
|
|
FCMService = require("~/lib/services/fcmService").FCMService;
|
|
}
|
|
|
|
export default function GoogleSetup() {
|
|
const { user, refreshProfile, refreshWallet } = useAuthWithProfile();
|
|
const { signOut } = useAuthStore();
|
|
|
|
// Pre-fill with Google account info if available
|
|
const [fullName, setFullName] = useState(user?.displayName || "");
|
|
const [phoneNumber, setPhoneNumber] = 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 [profileImage, setProfileImage] = useState<string | null>(null);
|
|
|
|
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);
|
|
|
|
console.log("GOOGLE SETUP PAGE LOADED, user:", user?.uid);
|
|
|
|
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(() => {
|
|
if (!user) {
|
|
console.log("NO USER FOUND, redirecting to signin");
|
|
router.replace(ROUTES.SIGNIN);
|
|
}
|
|
}, [user]);
|
|
|
|
// Update fullName when user data becomes available
|
|
useEffect(() => {
|
|
if (user?.displayName && !fullName) {
|
|
setFullName(user.displayName);
|
|
}
|
|
}, [user?.displayName]);
|
|
|
|
const handleSelectProfileImage = async () => {
|
|
try {
|
|
const permissionResult =
|
|
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
if (!permissionResult.granted) {
|
|
showToast(
|
|
"Permission Required",
|
|
"Please allow access to your photo library to select a profile picture.",
|
|
"error"
|
|
);
|
|
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 google setup bypass: skipping profile creation and navigating to HOME"
|
|
);
|
|
router.replace(ROUTES.HOME);
|
|
return;
|
|
}
|
|
|
|
if (!user) {
|
|
showToast("Error", "No authenticated user found", "error");
|
|
return;
|
|
}
|
|
|
|
// Validate full name
|
|
const fullNameResult = validate(fullNameSchema, fullName);
|
|
if (!fullNameResult.success) {
|
|
showToast("Error", fullNameResult.error, "warning");
|
|
return;
|
|
}
|
|
|
|
// Validate phone number
|
|
const phoneResult = validate(phoneNumberSchema, phoneNumber);
|
|
if (!phoneResult.success) {
|
|
showToast("Error", phoneResult.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;
|
|
}
|
|
|
|
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: phoneNumber.trim(),
|
|
address: address.trim() || undefined,
|
|
email: user.email || "",
|
|
pin: pin.trim(),
|
|
signupType: "google",
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
photoUrl,
|
|
});
|
|
|
|
// Initialize FCM token for new user (native only)
|
|
if (Platform.OS !== "web" && FCMService) {
|
|
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);
|
|
}
|
|
} catch (fcmError) {
|
|
console.error("Error initializing FCM token:", fcmError);
|
|
}
|
|
}
|
|
|
|
// Create wallet for the user
|
|
await WalletService.createUserWallet(user.uid);
|
|
console.log("User wallet created successfully");
|
|
|
|
// 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(
|
|
"Error",
|
|
"Failed to complete profile setup. Please try again.",
|
|
"error"
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleBackPress = () => {
|
|
showToast(
|
|
"Cancel Setup",
|
|
"Are you sure you want to cancel? You will need to sign in again.",
|
|
"warning"
|
|
);
|
|
setTimeout(async () => {
|
|
try {
|
|
await signOut();
|
|
router.replace(ROUTES.SIGNIN);
|
|
} catch (error) {
|
|
console.error("Error signing out:", error);
|
|
router.replace(ROUTES.SIGNIN);
|
|
}
|
|
}, 1500);
|
|
};
|
|
|
|
if (!user) {
|
|
return null; // Will redirect to signin
|
|
}
|
|
|
|
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">
|
|
Welcome to AmbaPay!
|
|
</Text>
|
|
<View className="h-4" />
|
|
<Text className="text-base font-dmsans text-gray-400">
|
|
Just a few more details to get you started.
|
|
</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">
|
|
Full Name *
|
|
</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder="Your full name"
|
|
value={fullName}
|
|
onChangeText={setFullName}
|
|
containerClassName="w-full"
|
|
placeholderColor="#9CA3AF"
|
|
placeholderClassName="text-xs"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
|
|
<View className="h-6" />
|
|
|
|
<Label className="text-base font-dmsans-medium">Email</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder="Email from Google account"
|
|
value={user.email || ""}
|
|
editable={false}
|
|
containerClassName="w-full"
|
|
placeholderColor="#9CA3AF"
|
|
placeholderClassName="text-xs"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
|
|
<View className="h-6" />
|
|
|
|
<Label className="text-base font-dmsans-medium">
|
|
Phone Number *
|
|
</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder="+251912345678"
|
|
value={phoneNumber}
|
|
onChangeText={setPhoneNumber}
|
|
keyboardType="phone-pad"
|
|
containerClassName="w-full"
|
|
placeholderColor="#9CA3AF"
|
|
placeholderClassName="text-xs"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
|
|
<View className="h-6" />
|
|
|
|
<Label className="text-base font-dmsans-medium">Address</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder="Addis Ababa, Ethiopia"
|
|
value={address}
|
|
onChangeText={setAddress}
|
|
containerClassName="w-full"
|
|
placeholderColor="#9CA3AF"
|
|
placeholderClassName="text-xs"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
|
|
<View className="h-6" />
|
|
|
|
<Label className="text-base font-dmsans-medium">PIN *</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder="Enter 6-digit PIN"
|
|
value={pin}
|
|
onChangeText={setPin}
|
|
keyboardType="numeric"
|
|
secureTextEntry={true}
|
|
maxLength={6}
|
|
containerClassName="w-full"
|
|
placeholderColor="#9CA3AF"
|
|
placeholderClassName="text-xs"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
|
|
<View className="h-6" />
|
|
|
|
<Label className="text-base font-dmsans-medium">
|
|
Confirm PIN *
|
|
</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder="Confirm your PIN"
|
|
value={confirmPin}
|
|
onChangeText={setConfirmPin}
|
|
keyboardType="numeric"
|
|
secureTextEntry={true}
|
|
maxLength={6}
|
|
containerClassName="w-full"
|
|
placeholderColor="#9CA3AF"
|
|
placeholderClassName="text-xs"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
|
|
<View className="h-8" />
|
|
|
|
<Button
|
|
className="bg-primary rounded-2xl"
|
|
onPress={handleCompleteSetup}
|
|
disabled={
|
|
loading ||
|
|
!fullName.trim() ||
|
|
!phoneNumber.trim() ||
|
|
!pin.trim() ||
|
|
!confirmPin.trim()
|
|
}
|
|
>
|
|
<Text className="font-dmsans text-white">
|
|
{loading ? "Setting up..." : "Get Started"}
|
|
</Text>
|
|
</Button>
|
|
|
|
<View className="h-8" />
|
|
</ScrollView>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
</KeyboardAvoidingView>
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|