421 lines
14 KiB
TypeScript
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;
|