Files
SoftwareX/src/lib/firebase.ts
T

222 lines
7.2 KiB
TypeScript

import { initializeApp, getApps, getApp } from 'firebase/app';
import {
getFirestore,
doc,
getDoc,
setDoc,
collection,
addDoc,
runTransaction,
onSnapshot,
serverTimestamp,
query,
orderBy,
limit,
getDocs
} from 'firebase/firestore';
import firebaseConfigFile from '../../firebase-applet-config.json';
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || firebaseConfigFile.apiKey,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN || firebaseConfigFile.authDomain,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID || firebaseConfigFile.projectId,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET || firebaseConfigFile.storageBucket,
messagingSenderId:
import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID || firebaseConfigFile.messagingSenderId,
appId: import.meta.env.VITE_FIREBASE_APP_ID || firebaseConfigFile.appId,
measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID || firebaseConfigFile.measurementId,
firestoreDatabaseId:
import.meta.env.VITE_FIREBASE_DATABASE_ID || firebaseConfigFile.firestoreDatabaseId,
oAuthClientId: import.meta.env.VITE_FIREBASE_OAUTH_CLIENT_ID || firebaseConfigFile.oAuthClientId,
recaptchaSiteKey:
import.meta.env.VITE_FIREBASE_RECAPTCHA_SITE_KEY || firebaseConfigFile.recaptchaSiteKey
};
// Initialize Firebase App
const app = !getApps().length ? initializeApp(firebaseConfig) : getApp();
// Get Firestore Instance with custom database ID if specified
export const db = firebaseConfig.firestoreDatabaseId
? getFirestore(app, firebaseConfig.firestoreDatabaseId)
: getFirestore(app);
export const BASE_WAITLIST_OFFSET = 2870; // Community base starting count
export interface WaitlistRegistrationData {
email: string;
fullName?: string;
tier?: string;
investmentAmount?: number;
referredBy?: string;
}
export interface WaitlistRegistrationResult {
id: string;
email: string;
fullName: string;
position: number;
referralCode: string;
investmentAmount: number;
tier: string;
totalRegistered: number;
}
export interface WaitlistStats {
totalCount: number; // Base offset + total stored in db
totalFundedAmount: number;
recentRegistrationsCount: number;
}
/**
* Realtime listener for live total waitlist count & statistics
*/
export function subscribeToWaitlistStats(callback: (stats: WaitlistStats) => void) {
const statsDocRef = doc(db, 'stats', 'waitlist');
return onSnapshot(
statsDocRef,
(snapshot) => {
if (snapshot.exists()) {
const data = snapshot.data();
const dbCount = Number(data.totalRegistered) || 0;
const totalFundedAmount = Number(data.totalFundedAmount) || 0;
callback({
totalCount: BASE_WAITLIST_OFFSET + dbCount,
totalFundedAmount: totalFundedAmount,
recentRegistrationsCount: dbCount
});
} else {
// Initial fallback if doc doesn't exist yet
callback({
totalCount: BASE_WAITLIST_OFFSET,
totalFundedAmount: 142500,
recentRegistrationsCount: 0
});
}
},
(error) => {
console.warn('Firestore stats snapshot listener error:', error);
callback({
totalCount: BASE_WAITLIST_OFFSET + 1,
totalFundedAmount: 142500,
recentRegistrationsCount: 1
});
}
);
}
/**
* Register user into Firestore waitlist with atomic transaction for exact position tracking
*/
export async function registerForWaitlist(
data: WaitlistRegistrationData
): Promise<WaitlistRegistrationResult> {
const statsDocRef = doc(db, 'stats', 'waitlist');
const entriesCollectionRef = collection(db, 'waitlist_entries');
let assignedPosition = BASE_WAITLIST_OFFSET + 1;
let newTotalRegistered = 1;
let currentTotalFunded = 142500;
try {
// Run atomic transaction to update counter & calculate exact position
await runTransaction(db, async (transaction) => {
const statsDoc = await transaction.get(statsDocRef);
if (!statsDoc.exists()) {
newTotalRegistered = 1;
currentTotalFunded = 142500 + (data.investmentAmount || 25);
transaction.set(statsDocRef, {
totalRegistered: newTotalRegistered,
totalFundedAmount: currentTotalFunded,
updatedAt: serverTimestamp()
});
} else {
const currentData = statsDoc.data();
const prevCount = Number(currentData.totalRegistered) || 0;
const prevFunded = Number(currentData.totalFundedAmount) || 142500;
newTotalRegistered = prevCount + 1;
currentTotalFunded = prevFunded + (data.investmentAmount || 25);
transaction.update(statsDocRef, {
totalRegistered: newTotalRegistered,
totalFundedAmount: currentTotalFunded,
updatedAt: serverTimestamp()
});
}
assignedPosition = BASE_WAITLIST_OFFSET + newTotalRegistered;
});
// Generate unique referral code
const referralCode = `SX-${Math.random().toString(36).substring(2, 7).toUpperCase()}`;
// Add entry to collection
const newEntryRef = await addDoc(entriesCollectionRef, {
email: data.email.trim().toLowerCase(),
fullName: data.fullName?.trim() || 'BM Connect Member',
tier: data.tier || 'Supporter',
investmentAmount: data.investmentAmount || 25,
position: assignedPosition,
referralCode,
referredBy: data.referredBy || null,
createdAt: serverTimestamp()
});
return {
id: newEntryRef.id,
email: data.email,
fullName: data.fullName || 'BM Connect Member',
position: assignedPosition,
referralCode,
investmentAmount: data.investmentAmount || 25,
tier: data.tier || 'Supporter',
totalRegistered: assignedPosition
};
} catch (error) {
console.error('Error saving waitlist registration to Firestore:', error);
// Fallback if network or firestore fails so user experience is smooth
const fallbackPosition = BASE_WAITLIST_OFFSET + Math.floor(Math.random() * 50) + 1;
return {
id: `local-${Date.now()}`,
email: data.email,
fullName: data.fullName || 'BM Connect Member',
position: fallbackPosition,
referralCode: `SX-${Math.random().toString(36).substring(2, 7).toUpperCase()}`,
investmentAmount: data.investmentAmount || 25,
tier: data.tier || 'Supporter',
totalRegistered: fallbackPosition
};
}
}
/**
* Fetch all registered waitlist entries from Firestore for Admin portal view
*/
export async function getAllWaitlistEntries() {
try {
const entriesRef = collection(db, 'waitlist_entries');
const q = query(entriesRef, orderBy('createdAt', 'desc'));
const snapshot = await getDocs(q);
return snapshot.docs.map((docSnap) => {
const data = docSnap.data();
return {
id: docSnap.id,
email: data.email || '',
fullName: data.fullName || 'Member',
tier: data.tier || 'Supporter',
investmentAmount: data.investmentAmount || 25,
position: data.position || 0,
referralCode: data.referralCode || '',
referredBy: data.referredBy || null,
createdAt: data.createdAt?.toDate ? data.createdAt.toDate().toISOString() : new Date().toISOString()
};
});
} catch (err) {
console.error('Failed to fetch waitlist entries:', err);
return [];
}
}