Amba-Agent-App/lib/firebase/firebase.web.ts
2026-01-16 00:22:35 +03:00

421 lines
14 KiB
TypeScript

/**
* Firebase Web Implementation
* Uses Firebase JS SDK for web platform
*/
import { initializeApp, getApps, getApp } from 'firebase/app';
import {
getAuth,
onAuthStateChanged as webOnAuthStateChanged,
GoogleAuthProvider,
signOut as webSignOut,
} from 'firebase/auth';
import type { User, ConfirmationResult } from 'firebase/auth';
import {
getFirestore,
collection as webCollection,
doc as webDoc,
getDoc,
setDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
getDocs,
onSnapshot,
serverTimestamp,
Timestamp as WebTimestamp,
deleteField
} from 'firebase/firestore';
import { getMessaging, getToken, onMessage } from 'firebase/messaging';
import { getFunctions, httpsCallable as webHttpsCallable } from 'firebase/functions';
// Firebase configuration for web
const firebaseConfig = {
apiKey: "AIzaSyCVprX0NvjjemRKRpG1ZJHyMwKsJmBuXHc",
authDomain: "ambapaydemo.firebaseapp.com",
databaseURL: "https://ambapaydemo-default-rtdb.europe-west1.firebasedatabase.app",
projectId: "ambapaydemo",
storageBucket: "ambapaydemo.firebasestorage.app",
messagingSenderId: "613864011564",
appId: "1:613864011564:web:e078c5990d3b2bff249e89",
measurementId: "G-F8RVT1BHHC"
};
// Initialize Firebase (only once)
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
const authInstance = getAuth(app);
const firestoreInstance = getFirestore(app);
// Lazy initialization for messaging (requires browser support)
let messagingInstance: ReturnType<typeof getMessaging> | null = null;
const getMessagingInstanceInternal = () => {
if (typeof window !== 'undefined' && 'Notification' in window) {
if (!messagingInstance) {
try {
messagingInstance = getMessaging(app);
} catch (e) {
console.warn('Firebase Messaging not available:', e);
}
}
return messagingInstance;
}
return null;
};
const functionsInstance = getFunctions(app);
// Type compatibility layer
export interface FirebaseAuthTypes {
User: User;
ConfirmationResult: ConfirmationResult;
}
// Status codes compatibility (for Google Sign-In error handling)
export const statusCodes = {
SIGN_IN_CANCELLED: 'SIGN_IN_CANCELLED',
IN_PROGRESS: 'IN_PROGRESS',
PLAY_SERVICES_NOT_AVAILABLE: 'PLAY_SERVICES_NOT_AVAILABLE',
};
// Auth exports
export const firebaseAuth = { GoogleAuthProvider };
export const getAuthInstance = () => authInstance;
export const onAuthStateChanged = (callback: (user: User | null) => void) => {
return webOnAuthStateChanged(authInstance, callback);
};
// Firestore exports
export const firebaseFirestore = {
FieldValue: {
serverTimestamp: () => serverTimestamp(),
delete: () => deleteField(),
}
};
export const getFirestoreInstance = () => firestoreInstance;
export const FieldValue = {
serverTimestamp: () => serverTimestamp(),
delete: () => deleteField(),
};
export const Timestamp = WebTimestamp;
// Collection helpers that return a Firestore-like interface
export const collection = (path: string) => {
const collectionRef = webCollection(firestoreInstance, path);
return {
doc: (docId: string) => createDocRef(path, docId),
where: (field: string, op: any, value: any) => {
return createQueryBuilder(collectionRef, [where(field, op, value)]);
},
orderBy: (field: string, direction?: 'asc' | 'desc') => {
return createQueryBuilder(collectionRef, [orderBy(field, direction)]);
},
get: async () => {
const snapshot = await getDocs(collectionRef);
return {
docs: snapshot.docs.map(docItem => ({
id: docItem.id,
data: () => docItem.data(),
exists: docItem.exists(),
})),
empty: snapshot.empty,
forEach: (callback: (docItem: any) => void) => {
snapshot.forEach(docItem => callback({
id: docItem.id,
data: () => docItem.data(),
exists: docItem.exists(),
}));
},
};
},
};
};
// Query builder for chaining
const createQueryBuilder = (collectionRef: any, constraints: any[] = []) => {
return {
where: (field: string, op: any, value: any) => {
return createQueryBuilder(collectionRef, [...constraints, where(field, op, value)]);
},
orderBy: (field: string, direction?: 'asc' | 'desc') => {
return createQueryBuilder(collectionRef, [...constraints, orderBy(field, direction)]);
},
limit: (n: number) => {
return createQueryBuilder(collectionRef, [...constraints, limit(n)]);
},
get: async () => {
const q = query(collectionRef, ...constraints);
const snapshot = await getDocs(q);
return {
docs: snapshot.docs.map(docItem => ({
id: docItem.id,
data: () => docItem.data(),
exists: docItem.exists(),
})),
empty: snapshot.empty,
forEach: (callback: (docItem: any) => void) => {
snapshot.forEach(docItem => callback({
id: docItem.id,
data: () => docItem.data(),
exists: docItem.exists(),
}));
},
};
},
onSnapshot: (callback: (snapshot: any) => void, errorCallback?: (error: any) => void) => {
const q = query(collectionRef, ...constraints);
return onSnapshot(q, (snapshot) => {
callback({
docs: snapshot.docs.map(docItem => ({
id: docItem.id,
data: () => docItem.data(),
exists: docItem.exists(),
})),
empty: snapshot.empty,
forEach: (cb: (docItem: any) => void) => {
snapshot.forEach(docItem => cb({
id: docItem.id,
data: () => docItem.data(),
exists: docItem.exists(),
}));
},
});
}, errorCallback);
},
};
};
// Document reference helper
const createDocRef = (collectionPath: string, docId: string) => {
const docRef = webDoc(firestoreInstance, collectionPath, docId);
return {
get: async () => {
const snapshot = await getDoc(docRef);
return {
exists: snapshot.exists(),
data: () => snapshot.data(),
id: snapshot.id,
};
},
set: async (data: any, options?: { merge?: boolean }) => {
await setDoc(docRef, data, options || {});
},
update: async (data: any) => {
await updateDoc(docRef, data);
},
delete: async () => {
await deleteDoc(docRef);
},
onSnapshot: (callback: (snapshot: any) => void, errorCallback?: (error: any) => void) => {
return onSnapshot(docRef, (snapshot) => {
callback({
exists: snapshot.exists(),
data: () => snapshot.data(),
id: snapshot.id,
});
}, errorCallback);
},
};
};
export const doc = (collectionPath: string, docId: string) => createDocRef(collectionPath, docId);
// Messaging exports (web-specific)
export const firebaseMessaging = {
AuthorizationStatus: {
AUTHORIZED: 1,
PROVISIONAL: 2,
DENIED: 0,
NOT_DETERMINED: -1,
},
};
export const getMessagingInstance = getMessagingInstanceInternal;
export const AuthorizationStatus = firebaseMessaging.AuthorizationStatus;
// Functions exports
export const firebaseFunctions = {};
export const getFunctionsInstance = () => functionsInstance;
export const httpsCallable = (name: string) => {
const callable = webHttpsCallable(functionsInstance, name);
return async (data: any) => {
const result = await callable(data);
return result;
};
};
// Google Sign-In (Web)
export const signInWithGoogle = async (): Promise<{
user: User | null;
isNewUser: boolean;
error?: string;
}> => {
try {
// Dynamic import for signInWithPopup (tree-shaking friendly)
const authModule = await import('firebase/auth');
const signInWithPopup = (authModule as any).signInWithPopup;
if (!signInWithPopup) {
return { user: null, isNewUser: false, error: 'signInWithPopup not available' };
}
const provider = new GoogleAuthProvider();
provider.setCustomParameters({
prompt: 'select_account'
});
const result = await signInWithPopup(authInstance, provider);
// Check if new user - web SDK doesn't directly expose this, check metadata
const isNewUser = result.user.metadata.creationTime === result.user.metadata.lastSignInTime;
return { user: result.user, isNewUser };
} catch (error: any) {
console.error('Google Sign-In error:', error);
if (error.code === 'auth/popup-closed-by-user') {
return { user: null, isNewUser: false, error: 'Sign in was cancelled' };
}
if (error.code === 'auth/popup-blocked') {
return { user: null, isNewUser: false, error: 'Popup was blocked. Please allow popups for this site.' };
}
return { user: null, isNewUser: false, error: error.message || 'Google Sign-In failed' };
}
};
export const signOutFromGoogle = async (): Promise<void> => {
// No separate Google sign out needed on web
};
// Phone Auth (Web) - requires reCAPTCHA
let recaptchaVerifier: any = null;
export const initRecaptcha = async (containerId: string): Promise<any> => {
if (typeof window !== 'undefined' && !recaptchaVerifier) {
// Dynamic import for RecaptchaVerifier
const authModule = await import('firebase/auth');
const RecaptchaVerifier = (authModule as any).RecaptchaVerifier;
if (RecaptchaVerifier) {
recaptchaVerifier = new RecaptchaVerifier(authInstance, containerId, {
size: 'invisible',
callback: () => {
console.log('reCAPTCHA verified');
},
});
}
}
return recaptchaVerifier;
};
export const signInWithPhoneNumber = async (phoneNumber: string): Promise<ConfirmationResult> => {
if (!recaptchaVerifier) {
throw new Error('reCAPTCHA not initialized. Call initRecaptcha first.');
}
// Dynamic import for signInWithPhoneNumber
const authModule = await import('firebase/auth');
const webSignInWithPhoneNumber = (authModule as any).signInWithPhoneNumber;
if (!webSignInWithPhoneNumber) {
throw new Error('signInWithPhoneNumber not available');
}
return webSignInWithPhoneNumber(authInstance, phoneNumber, recaptchaVerifier);
};
// Email/Password Authentication (Web)
export const createUserWithEmailAndPassword = async (
email: string,
password: string
): Promise<{ user: User | null; error?: string }> => {
try {
const authModule = await import('firebase/auth');
const createUser = (authModule as any).createUserWithEmailAndPassword;
if (!createUser) {
return { user: null, error: 'createUserWithEmailAndPassword not available' };
}
const userCredential = await createUser(authInstance, email, password);
return { user: userCredential.user };
} catch (error: any) {
console.error('Email/Password signup error:', error);
return {
user: null,
error: error.message || 'Failed to create account',
};
}
};
export const signInWithEmailAndPassword = async (
email: string,
password: string
): Promise<{ user: User | null; error?: string }> => {
try {
const authModule = await import('firebase/auth');
const signIn = (authModule as any).signInWithEmailAndPassword;
if (!signIn) {
return { user: null, error: 'signInWithEmailAndPassword not available' };
}
const userCredential = await signIn(authInstance, email, password);
return { user: userCredential.user };
} catch (error: any) {
console.error('Email/Password signin error:', error);
return {
user: null,
error: error.message || 'Failed to sign in',
};
}
};
// Firebase Auth sign out
export const signOut = async (): Promise<void> => {
await webSignOut(authInstance);
};
// Web FCM helpers
export const requestNotificationPermission = async (): Promise<boolean> => {
if (typeof window === 'undefined' || !('Notification' in window)) {
return false;
}
try {
const permission = await Notification.requestPermission();
return permission === 'granted';
} catch (error) {
console.error('Error requesting notification permission:', error);
return false;
}
};
export const getWebFCMToken = async (): Promise<string | null> => {
const messaging = getMessagingInstanceInternal();
if (!messaging) return null;
try {
// You'll need to add your VAPID key here for web push
const token = await getToken(messaging, {
vapidKey: 'YOUR_VAPID_KEY_HERE' // TODO: Add your VAPID key
});
return token;
} catch (error) {
console.error('Error getting FCM token:', error);
return null;
}
};
export const onWebMessage = (callback: (payload: any) => void) => {
const messaging = getMessagingInstanceInternal();
if (!messaging) return () => { };
return onMessage(messaging, callback);
};
// Platform identifier
export const isNative = false;
export const isWeb = true;