458 lines
14 KiB
TypeScript
458 lines
14 KiB
TypeScript
/**
|
|
* Auth Services - Platform-aware authentication
|
|
* Works on both native (Android/iOS) and web platforms
|
|
*/
|
|
import { Platform } from "react-native";
|
|
import {
|
|
signInWithGoogle as firebaseSignInWithGoogle,
|
|
signOutFromGoogle as firebaseSignOutFromGoogle,
|
|
signInWithPhoneNumber as firebaseSignInWithPhoneNumber,
|
|
createUserWithEmailAndPassword as firebaseCreateUserWithEmailAndPassword,
|
|
signInWithEmailAndPassword as firebaseSignInWithEmailAndPassword,
|
|
collection,
|
|
doc,
|
|
FieldValue,
|
|
isWeb,
|
|
} from "../firebase";
|
|
import { formatPhoneNumber } from "../utils/phoneUtils";
|
|
import { useUserProfileStore } from "../stores/userProfileStore";
|
|
import { withGlobalLoading } from "../stores/uiStore";
|
|
|
|
// Types
|
|
interface PhoneAuthResult {
|
|
confirmationResult?: any;
|
|
error?: string;
|
|
}
|
|
|
|
interface OTPVerificationResult {
|
|
user: any | null;
|
|
error?: string;
|
|
}
|
|
|
|
interface GoogleSignInResult {
|
|
user: any | null;
|
|
isNewUser: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export interface UserLinkedAccount {
|
|
id: string;
|
|
bankId: string;
|
|
bankName: string;
|
|
accountNumber: string;
|
|
isDefault: boolean;
|
|
}
|
|
|
|
export interface UserProfile {
|
|
uid: string;
|
|
email: string;
|
|
fullName: string;
|
|
phoneNumber?: string;
|
|
address?: string;
|
|
pin?: string;
|
|
signupType?: "phone" | "google";
|
|
photoUrl?: string;
|
|
fanNumber?: string;
|
|
tin?: string;
|
|
businessType?: string;
|
|
nationalIdUrl?: string;
|
|
businessLicenseUrl?: string;
|
|
linkedAccounts?: UserLinkedAccount[];
|
|
createdAt: any;
|
|
updatedAt: any;
|
|
}
|
|
|
|
export class AuthService {
|
|
// Google Sign-In - Platform aware
|
|
static async signInWithGoogle(): Promise<GoogleSignInResult> {
|
|
try {
|
|
const result = await firebaseSignInWithGoogle();
|
|
|
|
if (result.error) {
|
|
return { user: null, isNewUser: false, error: result.error };
|
|
}
|
|
|
|
console.log(
|
|
"Google Sign-In successful, user:",
|
|
result.user?.uid,
|
|
"isNewUser:",
|
|
result.isNewUser
|
|
);
|
|
|
|
// Log ID token if available (for debugging / backend integration)
|
|
try {
|
|
if (
|
|
result.user &&
|
|
typeof (result.user as any).getIdToken === "function"
|
|
) {
|
|
const idToken = await (result.user as any).getIdToken();
|
|
console.log("GOOGLE SIGN-IN ID TOKEN:", idToken);
|
|
}
|
|
} catch (tokenError) {
|
|
console.warn("Failed to get Google ID token:", tokenError);
|
|
}
|
|
return result;
|
|
} catch (error: any) {
|
|
console.error("Google Sign-In error:", error);
|
|
return {
|
|
user: null,
|
|
isNewUser: false,
|
|
error: error.message || "Google Sign-In failed",
|
|
};
|
|
}
|
|
}
|
|
|
|
// Sign out from Google
|
|
static async signOutFromGoogle(): Promise<void> {
|
|
try {
|
|
await firebaseSignOutFromGoogle();
|
|
} catch (error) {
|
|
console.error("Google Sign-Out error:", error);
|
|
}
|
|
}
|
|
|
|
// Phone number authentication - send OTP
|
|
static async sendOTP(phoneNumber: string): Promise<PhoneAuthResult> {
|
|
try {
|
|
// Validate and format phone number
|
|
const formattedPhoneNumber = formatPhoneNumber(phoneNumber);
|
|
console.log("FORMATTED PHONE NUMBER:", formattedPhoneNumber);
|
|
|
|
if (!formattedPhoneNumber) {
|
|
return {
|
|
error: "Invalid phone number format. Please use format: +1234567890",
|
|
};
|
|
}
|
|
|
|
console.log("Sending OTP to:", formattedPhoneNumber);
|
|
|
|
// Web requires reCAPTCHA initialization first
|
|
if (isWeb) {
|
|
// Check if reCAPTCHA container exists
|
|
const recaptchaContainer = document.getElementById(
|
|
"recaptcha-container"
|
|
);
|
|
if (!recaptchaContainer) {
|
|
console.warn(
|
|
"reCAPTCHA container not found. Phone auth may not work."
|
|
);
|
|
return {
|
|
error:
|
|
"Phone authentication not available. Please use Google Sign-In.",
|
|
};
|
|
}
|
|
|
|
// Initialize reCAPTCHA if needed
|
|
const { initRecaptcha } = await import("../firebase/firebase.web");
|
|
await initRecaptcha("recaptcha-container");
|
|
}
|
|
|
|
const confirmationResult = await firebaseSignInWithPhoneNumber(
|
|
formattedPhoneNumber
|
|
);
|
|
return { confirmationResult };
|
|
} catch (error) {
|
|
console.error("Phone auth error:", error);
|
|
return {
|
|
error: error instanceof Error ? error.message : "Failed to send OTP",
|
|
};
|
|
}
|
|
}
|
|
|
|
// Verify OTP code
|
|
static async verifyOTP(
|
|
confirmationResult: any,
|
|
code: string
|
|
): Promise<OTPVerificationResult> {
|
|
try {
|
|
const result = await confirmationResult.confirm(code);
|
|
const user = result?.user ?? null;
|
|
if (user) {
|
|
console.log("PHONE AUTH SUCCESS, UID:", user.uid);
|
|
try {
|
|
if (typeof (user as any).getIdToken === "function") {
|
|
const idToken = await (user as any).getIdToken();
|
|
console.log("PHONE AUTH ID TOKEN:", idToken);
|
|
}
|
|
} catch (tokenError) {
|
|
console.warn("Failed to get phone auth ID token:", tokenError);
|
|
}
|
|
}
|
|
return { user };
|
|
} catch (error) {
|
|
console.error("OTP verification error:", error);
|
|
return {
|
|
user: null,
|
|
error:
|
|
error instanceof Error ? error.message : "Invalid verification code",
|
|
};
|
|
}
|
|
}
|
|
|
|
// Update User Profile
|
|
static async updateUserProfile(
|
|
uid: string,
|
|
profileData: {
|
|
fullName?: string;
|
|
phoneNumber?: string;
|
|
address?: string;
|
|
pin?: string;
|
|
photoUrl?: string;
|
|
fanNumber?: string;
|
|
tin?: string;
|
|
businessType?: string;
|
|
nationalIdUrl?: string;
|
|
businessLicenseUrl?: string;
|
|
linkedAccounts?: UserLinkedAccount[];
|
|
}
|
|
): Promise<{ success: boolean; error?: string }> {
|
|
return withGlobalLoading(async () => {
|
|
try {
|
|
const userDocRef = doc("users", uid);
|
|
|
|
const updateData: any = {
|
|
...profileData,
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
};
|
|
|
|
// Remove undefined values
|
|
Object.keys(updateData).forEach((key) => {
|
|
if (updateData[key] === undefined) {
|
|
delete updateData[key];
|
|
}
|
|
});
|
|
|
|
console.log("[AuthService.updateUserProfile] Updating user profile", {
|
|
uid,
|
|
updateData,
|
|
});
|
|
|
|
await userDocRef.update(updateData);
|
|
console.log(
|
|
"[AuthService.updateUserProfile] Profile updated successfully"
|
|
);
|
|
|
|
try {
|
|
await useUserProfileStore.getState().invalidateProfile(uid);
|
|
} catch (cacheError) {
|
|
console.warn(
|
|
"Failed to refresh user profile cache after update",
|
|
cacheError
|
|
);
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error(
|
|
"[AuthService.updateUserProfile] Profile update error for uid",
|
|
uid,
|
|
error
|
|
);
|
|
return {
|
|
success: false,
|
|
error:
|
|
error instanceof Error ? error.message : "Failed to update profile",
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check if user profile exists in Firestore
|
|
static async checkUserProfileExists(uid: string): Promise<boolean> {
|
|
try {
|
|
const userDocRef = doc("users", uid);
|
|
const userDoc = await userDocRef.get();
|
|
// Handle both boolean and function forms of exists
|
|
const existsValue = userDoc.exists;
|
|
return typeof existsValue === "function" ? existsValue() : !!existsValue;
|
|
} catch (error) {
|
|
console.error("Error checking user profile:", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Create user profile in Firestore
|
|
static async createUserProfile(
|
|
uid: string,
|
|
profileData: {
|
|
fullName: string;
|
|
phoneNumber?: string;
|
|
address?: string;
|
|
email?: string;
|
|
pin?: string;
|
|
signupType?: "phone" | "google" | "email";
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
photoUrl?: string;
|
|
}
|
|
): Promise<void> {
|
|
return withGlobalLoading(async () => {
|
|
try {
|
|
console.log("Creating user profile in Firestore for UID:", uid);
|
|
const userProfile: UserProfile = {
|
|
uid: uid,
|
|
email: profileData.email || "",
|
|
fullName: profileData.fullName,
|
|
phoneNumber: profileData.phoneNumber || "",
|
|
address: profileData.address || "",
|
|
pin: profileData.pin || "",
|
|
signupType: profileData.signupType || "phone",
|
|
photoUrl: profileData.photoUrl || "",
|
|
createdAt: FieldValue.serverTimestamp(),
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
};
|
|
|
|
const userRef = doc("users", uid);
|
|
await userRef.set(userProfile);
|
|
console.log("User profile created successfully in Firestore");
|
|
|
|
try {
|
|
await useUserProfileStore.getState().invalidateProfile(uid);
|
|
} catch (cacheError) {
|
|
console.warn(
|
|
"Failed to refresh user profile cache after creation",
|
|
cacheError
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error creating user profile:", error);
|
|
throw error;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Agent Sign Up with Email/Password
|
|
static async signUpAgent(
|
|
email: string,
|
|
password: string
|
|
): Promise<{ user: any | null; error?: string }> {
|
|
try {
|
|
const result = await firebaseCreateUserWithEmailAndPassword(
|
|
email,
|
|
password
|
|
);
|
|
|
|
if (result.error) {
|
|
return { user: null, error: result.error };
|
|
}
|
|
|
|
if (!result.user) {
|
|
return { user: null, error: "Failed to create user account" };
|
|
}
|
|
|
|
// Check if agent exists in agents collection
|
|
const agentDocRef = doc("agents", result.user.uid);
|
|
const agentDoc = await agentDocRef.get();
|
|
const agentExists = agentDoc.exists;
|
|
|
|
if (!agentExists) {
|
|
// If agent doesn't exist, create agent document
|
|
await agentDocRef.set({
|
|
uid: result.user.uid,
|
|
email: email,
|
|
createdAt: FieldValue.serverTimestamp(),
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
});
|
|
}
|
|
|
|
console.log("Agent signup successful, UID:", result.user.uid);
|
|
return { user: result.user };
|
|
} catch (error: any) {
|
|
console.error("Agent signup error:", error);
|
|
return {
|
|
user: null,
|
|
error: error.message || "Failed to sign up agent",
|
|
};
|
|
}
|
|
}
|
|
|
|
// Agent Sign In with Email/Password
|
|
static async signInAgent(
|
|
email: string,
|
|
password: string
|
|
): Promise<{ user: any | null; error?: string }> {
|
|
try {
|
|
console.log("Agent signin attempt for email:", email);
|
|
const result = await firebaseSignInWithEmailAndPassword(email.trim(), password);
|
|
|
|
if (result.error) {
|
|
console.error("Firebase signin error:", result.error);
|
|
// Map common Firebase errors to user-friendly messages
|
|
let errorMessage = result.error;
|
|
if (result.error.includes("auth/wrong-password") || result.error.includes("auth/invalid-credential")) {
|
|
errorMessage = "Invalid email or password";
|
|
} else if (result.error.includes("auth/user-not-found")) {
|
|
errorMessage = "No account found with this email";
|
|
} else if (result.error.includes("auth/invalid-email")) {
|
|
errorMessage = "Invalid email address";
|
|
} else if (result.error.includes("auth/too-many-requests")) {
|
|
errorMessage = "Too many failed attempts. Please try again later";
|
|
}
|
|
return { user: null, error: errorMessage };
|
|
}
|
|
|
|
if (!result.user) {
|
|
console.error("No user returned from Firebase signin");
|
|
return { user: null, error: "Failed to sign in" };
|
|
}
|
|
|
|
console.log("Firebase signin successful, UID:", result.user.uid);
|
|
|
|
// Check if agent exists in agents collection
|
|
const agentDocRef = doc("agents", result.user.uid);
|
|
const agentDoc = await agentDocRef.get();
|
|
const agentExists = typeof agentDoc.exists === "function" ? agentDoc.exists() : !!agentDoc.exists;
|
|
|
|
if (!agentExists) {
|
|
console.log("Agent document not found, creating it automatically");
|
|
// Create agent document if it doesn't exist
|
|
try {
|
|
await agentDocRef.set({
|
|
uid: result.user.uid,
|
|
email: email.trim(),
|
|
createdAt: FieldValue.serverTimestamp(),
|
|
updatedAt: FieldValue.serverTimestamp(),
|
|
});
|
|
console.log("Agent document created successfully");
|
|
} catch (createError) {
|
|
console.error("Error creating agent document:", createError);
|
|
// Still allow signin even if document creation fails
|
|
}
|
|
} else {
|
|
console.log("Agent document exists");
|
|
}
|
|
|
|
console.log("Agent signin successful, UID:", result.user.uid);
|
|
return { user: result.user };
|
|
} catch (error: any) {
|
|
console.error("Agent signin error:", error);
|
|
let errorMessage = error.message || "Failed to sign in agent";
|
|
// Map common Firebase errors
|
|
if (errorMessage.includes("auth/wrong-password") || errorMessage.includes("auth/invalid-credential")) {
|
|
errorMessage = "Invalid email or password";
|
|
} else if (errorMessage.includes("auth/user-not-found")) {
|
|
errorMessage = "No account found with this email";
|
|
}
|
|
return {
|
|
user: null,
|
|
error: errorMessage,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check if agent exists in agents collection
|
|
static async checkAgentExists(uid: string): Promise<boolean> {
|
|
try {
|
|
const agentDocRef = doc("agents", uid);
|
|
const agentDoc = await agentDocRef.get();
|
|
const existsValue = agentDoc.exists;
|
|
return typeof existsValue === "function" ? existsValue() : !!existsValue;
|
|
} catch (error) {
|
|
console.error("Error checking agent:", error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default AuthService;
|