🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples d'Intégration React Native Firebase
Exemples d'intégration React Native Firebase incluant l'authentification, la base de données temps réel, les cloud functions, les notifications push, l'analytics et le cloud storage
💻 Authentification React Native Firebase typescript
Implémenter une authentification Firebase complète incluant email/mot de passe, login social, authentification biométrique et gestion de session sécurisée
// firebase.config.ts
import { FirebaseOptions } from '@firebase/app';
const firebaseConfig: FirebaseOptions = {
apiKey: "your-api-key",
authDomain: "your-project.firebaseapp.com",
projectId: "your-project-id",
storageBucket: "your-project.appspot.com",
messagingSenderId: "1234567890",
appId: "your-app-id",
measurementId: "your-measurement-id"
};
export default firebaseConfig;
// src/services/AuthService.ts
import auth, {
FirebaseAuthTypes,
getAuth,
signInWithEmailAndPassword,
createUserWithEmailAndPassword,
signOut,
sendPasswordResetEmail,
updatePassword,
User,
AuthCredential,
GoogleAuthProvider,
FacebookAuthProvider,
TwitterAuthProvider,
OAuthProvider,
updateProfile,
linkWithCredential,
unlink as unlinkProvider
} from '@react-native-firebase/auth';
import { GoogleSignin } from '@react-native-google-signin/google-signin';
import { LoginManager, AccessToken } from 'react-native-fbsdk-next';
import { TwitterAuth } from '@react-native-firebase/auth';
import appleAuth, {
AppleAuthRequestOperation,
AppleAuthRequestScope,
AppleAuthCredentialState,
} from '@invertase/react-native-apple-authentication';
import { TouchID, TouchIDResponse } from 'react-native-touch-id';
import { BiometryTypes } from 'react-native-biometrics';
import AsyncStorage from '@react-native-async-storage/async-storage';
export interface UserProfile {
uid: string;
email: string;
displayName: string;
photoURL?: string;
emailVerified: boolean;
createdAt: string;
lastLoginAt: string;
providers: string[];
phoneNumber?: string;
}
export interface AuthError {
code: string;
message: string;
nativeErrorMessage?: string;
}
export interface BiometricConfig {
title: string;
subtitle?: string;
description?: string;
fallbackLabel?: string;
allowDeviceCredentials?: boolean;
}
class AuthService {
private static instance: AuthService;
private currentUser: User | null = null;
private authListeners: ((user: User | null) => void)[] = [];
private constructor() {
this.initializeAuth();
}
static getInstance(): AuthService {
if (!AuthService.instance) {
AuthService.instance = new AuthService();
}
return AuthService.instance;
}
private initializeAuth(): void {
// Set up authentication state listener
auth().onAuthStateChanged((user) => {
this.currentUser = user;
this.notifyAuthListeners(user);
if (user) {
this.updateLastLogin(user.uid);
}
});
}
private async updateLastLogin(uid: string): Promise<void> {
try {
await AsyncStorage.setItem(`lastLogin_${uid}`, Date.now().toString());
} catch (error) {
console.error('Failed to update last login:', error);
}
}
// Authentication State Management
onAuthStateChanged(callback: (user: User | null) => void): () => void {
this.authListeners.push(callback);
// Return unsubscribe function
return () => {
const index = this.authListeners.indexOf(callback);
if (index > -1) {
this.authListeners.splice(index, 1);
}
};
}
private notifyAuthListeners(user: User | null): void {
this.authListeners.forEach(callback => callback(user));
}
getCurrentUser(): User | null {
return this.currentUser;
}
// Email/Password Authentication
async signUpWithEmail(email: string, password: string, displayName?: string): Promise<User> {
try {
const userCredential = await createUserWithEmailAndPassword(auth(), email, password);
const user = userCredential.user;
if (displayName) {
await updateProfile(user, { displayName });
}
await this.sendEmailVerification();
await this.saveUserProfile(user);
return user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async signInWithEmail(email: string, password: string): Promise<User> {
try {
const userCredential = await signInWithEmailAndPassword(auth(), email, password);
await this.saveUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async sendEmailVerification(): Promise<void> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
await this.currentUser.sendEmailVerification();
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async sendPasswordResetEmail(email: string): Promise<void> {
try {
await sendPasswordResetEmail(auth(), email);
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async updatePassword(newPassword: string): Promise<void> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
await updatePassword(this.currentUser, newPassword);
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Google Authentication
async signInWithGoogle(): Promise<User> {
try {
// Check if your device supports Google Play Services
await GoogleSignin.hasPlayServices();
// Get the user's ID token
const { idToken } = await GoogleSignin.signIn();
// Create a Google credential with the token
const googleCredential = GoogleAuthProvider.credential(idToken);
// Sign-in the user with the credential
const userCredential = await auth().signInWithCredential(googleCredential);
await this.saveUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async linkGoogleAccount(): Promise<User> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
await GoogleSignin.hasPlayServices();
const { idToken } = await GoogleSignin.signIn();
const googleCredential = GoogleAuthProvider.credential(idToken);
const userCredential = await linkWithCredential(this.currentUser, googleCredential);
await this.updateUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Facebook Authentication
async signInWithFacebook(): Promise<User> {
try {
// Attempt login with permissions
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
if (result.isCancelled) {
throw new Error('User cancelled the login process');
}
// Once signed in, get the user's accessToken
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
throw new Error('Something went wrong obtaining access token');
}
// Create a Firebase credential with the AccessToken
const facebookCredential = FacebookAuthProvider.credential(data.accessToken);
// Sign-in the user with the credential
const userCredential = await auth().signInWithCredential(facebookCredential);
await this.saveUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async linkFacebookAccount(): Promise<User> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
const result = await LoginManager.logInWithPermissions(['public_profile', 'email']);
if (result.isCancelled) {
throw new Error('User cancelled the login process');
}
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
throw new Error('Something went wrong obtaining access token');
}
const facebookCredential = FacebookAuthProvider.credential(data.accessToken);
const userCredential = await linkWithCredential(this.currentUser, facebookCredential);
await this.updateUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Apple Authentication
async signInWithApple(): Promise<User> {
try {
// Start the sign-in request
const appleAuthRequestResponse = await appleAuth.performRequest({
requestedOperation: AppleAuthRequestOperation.SIGN_IN,
requestedScopes: [
AppleAuthRequestScope.EMAIL,
AppleAuthRequestScope.FULL_NAME,
],
});
// Ensure Apple returned a user identityToken
if (!appleAuthRequestResponse.identityToken) {
throw new Error('Apple Sign-In failed - no identity token returned');
}
// Create a Firebase credential from the response
const { identityToken, nonce } = appleAuthRequestResponse;
const appleCredential = OAuthProvider('apple.com').credential({
idToken: identityToken,
rawNonce: nonce,
});
// Sign the user in with the credential
const userCredential = await auth().signInWithCredential(appleCredential);
await this.saveUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Biometric Authentication
async setupBiometricAuth(): Promise<void> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
const config: BiometricConfig = {
title: 'Enable Biometric Authentication',
subtitle: 'Use your fingerprint or Face ID',
description: 'Quickly sign in with biometrics',
allowDeviceCredentials: true,
};
const optionalConfigObject = {
title: config.title,
imageColor: '#e00606',
imageErrorColor: '#ff0000',
sensorDescription: config.subtitle,
sensorErrorDescription: 'Failed',
cancelText: 'Cancel',
fallbackLabel: config.fallbackLabel || 'Show Password',
unifiedErrors: false,
passcodeFallback: config.allowDeviceCredentials || false,
};
const isSupported = await TouchID.isSupported();
if (isSupported) {
await TouchID.authenticate(
config.description || 'Authenticate to enable biometric login',
optionalConfigObject
);
// Store biometric preference
await AsyncStorage.setItem(
`biometricEnabled_${this.currentUser.uid}`,
'true'
);
}
} catch (error) {
throw new Error(`Biometric setup failed: ${error}`);
}
}
async signInWithBiometrics(): Promise<User> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
const biometricEnabled = await AsyncStorage.getItem(
`biometricEnabled_${this.currentUser.uid}`
);
if (!biometricEnabled) {
throw new Error('Biometric authentication not enabled');
}
const optionalConfigObject = {
title: 'Biometric Login',
imageColor: '#e00606',
imageErrorColor: '#ff0000',
sensorDescription: 'Touch sensor',
sensorErrorDescription: 'Failed',
cancelText: 'Cancel',
fallbackLabel: 'Use Password',
unifiedErrors: false,
passcodeFallback: true,
};
await TouchID.authenticate('Authenticate to continue', optionalConfigObject);
// Refresh the user session
await this.currentUser.reload();
return this.currentUser;
} catch (error) {
throw new Error(`Biometric authentication failed: ${error}`);
}
}
// Phone Number Authentication
async signInWithPhoneNumber(phoneNumber: string): Promise<FirebaseAuthTypes.ConfirmationResult> {
try {
const confirmation = await auth().signInWithPhoneNumber(phoneNumber);
return confirmation;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async confirmPhoneNumberCode(
confirmationResult: FirebaseAuthTypes.ConfirmationResult,
code: string
): Promise<User> {
try {
const userCredential = await confirmationResult.confirm(code);
await this.saveUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Profile Management
async updateUserProfile(displayName?: string, photoURL?: string): Promise<void> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
await updateProfile(this.currentUser, {
displayName,
photoURL,
});
await this.updateUserProfile(this.currentUser);
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Provider Management
async unlinkProvider(providerId: string): Promise<User> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
const userCredential = await unlinkProvider(this.currentUser, providerId);
await this.updateUserProfile(userCredential.user);
return userCredential.user;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async getProviders(): Promise<string[]> {
try {
if (!this.currentUser) {
return [];
}
const userInfo = await this.currentUser.reload();
const providerData = userInfo.providerData;
return providerData.map(provider => provider.providerId);
} catch (error) {
console.error('Failed to get providers:', error);
return [];
}
}
// User Profile Storage
private async saveUserProfile(user: User): Promise<void> {
try {
const profile: UserProfile = {
uid: user.uid,
email: user.email || '',
displayName: user.displayName || '',
photoURL: user.photoURL || '',
emailVerified: user.emailVerified,
createdAt: user.metadata.creationTime || new Date().toISOString(),
lastLoginAt: new Date().toISOString(),
providers: user.providerData.map(provider => provider.providerId),
phoneNumber: user.phoneNumber || undefined,
};
await AsyncStorage.setItem(`userProfile_${user.uid}`, JSON.stringify(profile));
} catch (error) {
console.error('Failed to save user profile:', error);
}
}
private async updateUserProfile(user: User): Promise<void> {
const existingProfileStr = await AsyncStorage.getItem(`userProfile_${user.uid}`);
const existingProfile = existingProfileStr ? JSON.parse(existingProfileStr) : {};
const updatedProfile: UserProfile = {
...existingProfile,
displayName: user.displayName || existingProfile.displayName,
photoURL: user.photoURL || existingProfile.photoURL,
emailVerified: user.emailVerified,
phoneNumber: user.phoneNumber || existingProfile.phoneNumber,
lastLoginAt: new Date().toISOString(),
providers: user.providerData.map(provider => provider.providerId),
};
await AsyncStorage.setItem(
`userProfile_${user.uid}`,
JSON.stringify(updatedProfile)
);
}
async getUserProfile(uid: string): Promise<UserProfile | null> {
try {
const profileStr = await AsyncStorage.getItem(`userProfile_${uid}`);
return profileStr ? JSON.parse(profileStr) : null;
} catch (error) {
console.error('Failed to get user profile:', error);
return null;
}
}
// Sign Out
async signOut(): Promise<void> {
try {
await signOut(auth());
this.currentUser = null;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
// Error Handling
private handleAuthError(error: AuthError): Error {
const errorCode = error.code;
let errorMessage = 'An unknown error occurred';
switch (errorCode) {
case 'auth/user-not-found':
errorMessage = 'No user found with this email address';
break;
case 'auth/wrong-password':
errorMessage = 'Invalid password';
break;
case 'auth/email-already-in-use':
errorMessage = 'This email is already in use';
break;
case 'auth/weak-password':
errorMessage = 'Password should be at least 6 characters';
break;
case 'auth/invalid-email':
errorMessage = 'Invalid email address';
break;
case 'auth/user-disabled':
errorMessage = 'This user account has been disabled';
break;
case 'auth/too-many-requests':
errorMessage = 'Too many failed attempts. Please try again later';
break;
case 'auth/network-request-failed':
errorMessage = 'Network error. Please check your connection';
break;
case 'auth/invalid-verification-code':
errorMessage = 'Invalid verification code';
break;
case 'auth/code-expired':
errorMessage = 'Verification code has expired';
break;
default:
errorMessage = error.message || errorMessage;
}
return new Error(errorMessage);
}
// Utility Methods
isEmailVerified(): boolean {
return this.currentUser?.emailVerified || false;
}
async checkBiometricAvailability(): Promise<{
available: boolean;
biometryType: string | null;
}> {
try {
const isSupported = await TouchID.isSupported();
let biometryType = null;
if (isSupported) {
biometryType = await TouchID.isSupported();
}
return {
available: !!isSupported,
biometryType,
};
} catch (error) {
return {
available: false,
biometryType: null,
};
}
}
async resendVerificationEmail(): Promise<void> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
if (this.currentUser.emailVerified) {
throw new Error('Email is already verified');
}
await this.sendEmailVerification();
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
async deleteAccount(): Promise<void> {
try {
if (!this.currentUser) {
throw new Error('No authenticated user');
}
await this.currentUser.delete();
// Clean up stored data
await AsyncStorage.removeItem(`userProfile_${this.currentUser.uid}`);
await AsyncStorage.removeItem(`biometricEnabled_${this.currentUser.uid}`);
this.currentUser = null;
} catch (error) {
throw this.handleAuthError(error as AuthError);
}
}
}
export default AuthService;
// src/components/AuthScreen.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import { GoogleSigninButton } from '@react-native-google-signin/google-signin';
import AuthService, { BiometricConfig } from '../services/AuthService';
interface AuthScreenProps {
onAuthSuccess: () => void;
}
const AuthScreen: React.FC<AuthScreenProps> = ({ onAuthSuccess }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [isSignUp, setIsSignUp] = useState(false);
const [loading, setLoading] = useState(false);
const [biometricEnabled, setBiometricEnabled] = useState(false);
const authService = AuthService.getInstance();
useEffect(() => {
checkAuthState();
checkBiometricStatus();
}, []);
const checkAuthState = () => {
const unsubscribe = authService.onAuthStateChanged((user) => {
if (user && authService.isEmailVerified()) {
onAuthSuccess();
}
});
return () => unsubscribe();
};
const checkBiometricStatus = async () => {
if (authService.getCurrentUser()) {
const { available } = await authService.checkBiometricAvailability();
setBiometricEnabled(available);
}
};
const handleEmailAuth = async () => {
if (!email || !password || (isSignUp && !displayName)) {
Alert.alert('Error', 'Please fill in all required fields');
return;
}
setLoading(true);
try {
if (isSignUp) {
await authService.signUpWithEmail(email, password, displayName);
Alert.alert(
'Success',
'Account created! Please check your email for verification.',
[{ text: 'OK', onPress: () => setIsSignUp(false) }]
);
} else {
await authService.signInWithEmail(email, password);
if (!authService.isEmailVerified()) {
Alert.alert(
'Email Not Verified',
'Please verify your email before continuing.',
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Resend Email', onPress: handleResendVerification }
]
);
} else {
onAuthSuccess();
}
}
} catch (error) {
Alert.alert('Authentication Error', error.message);
} finally {
setLoading(false);
}
};
const handleGoogleAuth = async () => {
setLoading(true);
try {
await authService.signInWithGoogle();
onAuthSuccess();
} catch (error) {
Alert.alert('Google Sign-In Error', error.message);
} finally {
setLoading(false);
}
};
const handleFacebookAuth = async () => {
setLoading(true);
try {
await authService.signInWithFacebook();
onAuthSuccess();
} catch (error) {
Alert.alert('Facebook Sign-In Error', error.message);
} finally {
setLoading(false);
}
};
const handleAppleAuth = async () => {
setLoading(true);
try {
await authService.signInWithApple();
onAuthSuccess();
} catch (error) {
Alert.alert('Apple Sign-In Error', error.message);
} finally {
setLoading(false);
}
};
const handleBiometricAuth = async () => {
setLoading(true);
try {
await authService.signInWithBiometrics();
onAuthSuccess();
} catch (error) {
Alert.alert('Biometric Authentication Error', error.message);
} finally {
setLoading(false);
}
};
const handleResendVerification = async () => {
try {
await authService.resendVerificationEmail();
Alert.alert('Success', 'Verification email sent!');
} catch (error) {
Alert.alert('Error', error.message);
}
};
const handlePasswordReset = async () => {
if (!email) {
Alert.alert('Error', 'Please enter your email address');
return;
}
try {
await authService.sendPasswordResetEmail(email);
Alert.alert('Success', 'Password reset email sent!');
} catch (error) {
Alert.alert('Error', error.message);
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.header}>
<Text style={styles.title}>Welcome to Firebase Auth</Text>
<Text style={styles.subtitle}>
{isSignUp ? 'Create your account' : 'Sign in to continue'}
</Text>
</View>
<View style={styles.form}>
{isSignUp && (
<TextInput
style={styles.input}
placeholder="Display Name"
value={displayName}
onChangeText={setDisplayName}
autoCapitalize="words"
/>
)}
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity style={styles.primaryButton} onPress={handleEmailAuth}>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}>
{isSignUp ? 'Sign Up' : 'Sign In'}
</Text>
)}
</TouchableOpacity>
{!isSignUp && (
<TouchableOpacity style={styles.secondaryButton} onPress={handlePasswordReset}>
<Text style={styles.secondaryButtonText}>Forgot Password?</Text>
</TouchableOpacity>
)}
</View>
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>OR</Text>
<View style={styles.dividerLine} />
</View>
<View style={styles.socialButtons}>
<GoogleSigninButton
style={styles.googleButton}
size={GoogleSigninButton.Size.Wide}
color={GoogleSigninButton.Color.Dark}
onPress={handleGoogleAuth}
disabled={loading}
/>
<TouchableOpacity
style={[styles.socialButton, { backgroundColor: '#1877F2' }]}
onPress={handleFacebookAuth}
disabled={loading}
>
<Text style={styles.socialButtonText}>Continue with Facebook</Text>
</TouchableOpacity>
{Platform.OS === 'ios' && (
<TouchableOpacity
style={[styles.socialButton, { backgroundColor: '#000' }]}
onPress={handleAppleAuth}
disabled={loading}
>
<Text style={styles.socialButtonText}>Continue with Apple</Text>
</TouchableOpacity>
)}
{biometricEnabled && (
<TouchableOpacity
style={[styles.socialButton, { backgroundColor: '#4CAF50' }]}
onPress={handleBiometricAuth}
disabled={loading}
>
<Text style={styles.socialButtonText}>Use Biometric Login</Text>
</TouchableOpacity>
)}
</View>
<TouchableOpacity
style={styles.switchMode}
onPress={() => setIsSignUp(!isSignUp)}
>
<Text style={styles.switchModeText}>
{isSignUp
? 'Already have an account? Sign In'
: "Don't have an account? Sign Up"}
</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
scrollContainer: {
flexGrow: 1,
justifyContent: 'center',
padding: 20,
},
header: {
alignItems: 'center',
marginBottom: 40,
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#666',
},
form: {
marginBottom: 30,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 15,
fontSize: 16,
marginBottom: 15,
backgroundColor: '#fff',
},
primaryButton: {
backgroundColor: '#007AFF',
borderRadius: 8,
padding: 15,
alignItems: 'center',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
secondaryButton: {
marginTop: 10,
alignItems: 'center',
},
secondaryButtonText: {
color: '#007AFF',
fontSize: 14,
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: '#ddd',
},
dividerText: {
marginHorizontal: 10,
color: '#666',
},
socialButtons: {
marginBottom: 30,
},
googleButton: {
width: '100%',
height: 48,
marginBottom: 15,
},
socialButton: {
borderRadius: 8,
padding: 15,
alignItems: 'center',
marginBottom: 15,
},
socialButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
switchMode: {
alignItems: 'center',
},
switchModeText: {
color: '#007AFF',
fontSize: 14,
},
});
export default AuthScreen;
💻 Base de Données Temps Réel React Native Firebase typescript
Construire des applications collaboratives en temps réel avec Firebase Realtime Database incluant le support offline, la synchronisation de données et les requêtes complexes
// src/services/RealtimeDatabaseService.ts
import database, {
DatabaseReference,
DataSnapshot,
Query,
push,
remove,
set,
update,
get,
child,
orderByChild,
orderByKey,
orderByValue,
limitToFirst,
limitToLast,
startAt,
endAt,
equalTo,
onValue,
onChildAdded,
onChildChanged,
onChildRemoved,
onChildMoved,
serverTimestamp,
DatabaseReferenceType,
} from '@react-native-firebase/database';
import { Alert } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
export interface RealtimeData {
[key: string]: any;
}
export interface DatabaseNode {
key: string;
value: any;
timestamp?: number;
}
export interface QueryOptions {
orderBy?: 'child' | 'key' | 'value';
orderByChild?: string;
limitToFirst?: number;
limitToLast?: number;
startAt?: any;
startAfter?: any;
endAt?: any;
endBefore?: any;
equalTo?: any;
}
export interface TransactionResult {
committed: boolean;
snapshot?: DataSnapshot;
}
class RealtimeDatabaseService {
private static instance: RealtimeDatabaseService;
private database: DatabaseReference;
private offlineCache: Map<string, any> = new Map();
private constructor() {
this.database = database().ref();
this.initializeOfflineSupport();
}
static getInstance(): RealtimeDatabaseService {
if (!RealtimeDatabaseService.instance) {
RealtimeDatabaseService.instance = new RealtimeDatabaseService();
}
return RealtimeDatabaseService.instance;
}
private async initializeOfflineSupport(): Promise<void> {
try {
// Enable offline capabilities
await database().setPersistenceEnabled(true);
// Load cached data
await this.loadOfflineCache();
} catch (error) {
console.error('Failed to initialize offline support:', error);
}
}
private async loadOfflineCache(): Promise<void> {
try {
const cachedKeys = await AsyncStorage.getAllKeys();
const dbKeys = cachedKeys.filter(key => key.startsWith('db_cache_'));
for (const key of dbKeys) {
const value = await AsyncStorage.getItem(key);
if (value) {
const path = key.replace('db_cache_', '');
this.offlineCache.set(path, JSON.parse(value));
}
}
} catch (error) {
console.error('Failed to load offline cache:', error);
}
}
private async cacheOfflineData(path: string, data: any): Promise<void> {
try {
this.offlineCache.set(path, data);
await AsyncStorage.setItem(`db_cache_${path}`, JSON.stringify(data));
} catch (error) {
console.error('Failed to cache offline data:', error);
}
}
// Basic CRUD Operations
async createData(path: string, data: any, withTimestamp: boolean = true): Promise<string> {
try {
const finalData = withTimestamp
? { ...data, createdAt: serverTimestamp() }
: data;
const newRef = push(this.database.child(path), finalData);
// Cache the new data
await this.cacheOfflineData(`${path}/${newRef.key}`, finalData);
return newRef.key || '';
} catch (error) {
console.error('Failed to create data:', error);
throw new Error(`Failed to create data: ${error.message}`);
}
}
async readData(path: string): Promise<RealtimeData | null> {
try {
// Try offline cache first
const cachedData = this.offlineCache.get(path);
if (cachedData) {
return cachedData;
}
const snapshot: DataSnapshot = await get(this.database.child(path));
const data = snapshot.val();
if (data) {
await this.cacheOfflineData(path, data);
}
return data;
} catch (error) {
console.error('Failed to read data:', error);
// Fallback to offline cache
const cachedData = this.offlineCache.get(path);
if (cachedData) {
return cachedData;
}
throw new Error(`Failed to read data: ${error.message}`);
}
}
async updateData(path: string, data: any): Promise<void> {
try {
const updateData = {
...data,
updatedAt: serverTimestamp(),
};
await update(this.database.child(path), updateData);
// Update cache
const existingData = this.offlineCache.get(path) || {};
await this.cacheOfflineData(path, { ...existingData, ...updateData });
} catch (error) {
console.error('Failed to update data:', error);
throw new Error(`Failed to update data: ${error.message}`);
}
}
async setData(path: string, data: any, withTimestamp: boolean = true): Promise<void> {
try {
const finalData = withTimestamp
? { ...data, updatedAt: serverTimestamp() }
: data;
await set(this.database.child(path), finalData);
// Update cache
await this.cacheOfflineData(path, finalData);
} catch (error) {
console.error('Failed to set data:', error);
throw new Error(`Failed to set data: ${error.message}`);
}
}
async deleteData(path: string): Promise<void> {
try {
await remove(this.database.child(path));
// Remove from cache
this.offlineCache.delete(path);
await AsyncStorage.removeItem(`db_cache_${path}`);
} catch (error) {
console.error('Failed to delete data:', error);
throw new Error(`Failed to delete data: ${error.message}`);
}
}
// Query Operations
private buildQuery(path: string, options: QueryOptions): Query {
let query: Query = this.database.child(path);
// Order by
if (options.orderBy === 'child' && options.orderByChild) {
query = orderByChild(query, options.orderByChild);
} else if (options.orderBy === 'key') {
query = orderByKey(query);
} else if (options.orderBy === 'value') {
query = orderByValue(query);
}
// Limit
if (options.limitToFirst) {
query = limitToFirst(query, options.limitToFirst);
}
if (options.limitToLast) {
query = limitToLast(query, options.limitToLast);
}
// Range
if (options.startAt !== undefined) {
query = startAt(query, options.startAt);
}
if (options.startAfter !== undefined) {
query = startAt(query, options.startAfter);
}
if (options.endAt !== undefined) {
query = endAt(query, options.endAt);
}
if (options.endBefore !== undefined) {
query = endAt(query, options.endBefore);
}
// Equality
if (options.equalTo !== undefined) {
query = equalTo(query, options.equalTo);
}
return query;
}
async queryData(path: string, options: QueryOptions): Promise<RealtimeData[]> {
try {
const query = this.buildQuery(path, options);
const snapshot: DataSnapshot = await get(query);
const results: RealtimeData[] = [];
snapshot.forEach((childSnapshot) => {
results.push({
key: childSnapshot.key || '',
value: childSnapshot.val(),
});
});
return results;
} catch (error) {
console.error('Failed to query data:', error);
throw new Error(`Failed to query data: ${error.message}`);
}
}
// Real-time Listeners
onValueChange(
path: string,
callback: (data: RealtimeData | null) => void,
options?: QueryOptions
): () => void {
const query = options ? this.buildQuery(path, options) : this.database.child(path);
return onValue(query, (snapshot: DataSnapshot) => {
const data = snapshot.val();
callback(data);
// Update cache
if (data) {
this.cacheOfflineData(path, data);
}
});
}
onChildAdded(
path: string,
callback: (child: DatabaseNode) => void,
options?: QueryOptions
): () => void {
const query = options ? this.buildQuery(path, options) : this.database.child(path);
return onChildAdded(query, (snapshot: DataSnapshot, previousChildKey?: string) => {
const child = {
key: snapshot.key || '',
value: snapshot.val(),
previousChildKey,
};
callback(child);
});
}
onChildChanged(
path: string,
callback: (child: DatabaseNode) => void,
options?: QueryOptions
): () => void {
const query = options ? this.buildQuery(path, options) : this.database.child(path);
return onChildChanged(query, (snapshot: DataSnapshot, previousChildKey?: string) => {
const child = {
key: snapshot.key || '',
value: snapshot.val(),
previousChildKey,
};
callback(child);
});
}
onChildRemoved(
path: string,
callback: (child: DatabaseNode) => void,
options?: QueryOptions
): () => void {
const query = options ? this.buildQuery(path, options) : this.database.child(path);
return onChildRemoved(query, (snapshot: DataSnapshot) => {
const child = {
key: snapshot.key || '',
value: snapshot.val(),
};
callback(child);
});
}
// Transaction Operations
async runTransaction(
path: string,
transactionUpdate: (currentData: any) => any
): Promise<TransactionResult> {
try {
const result = await this.database.child(path).transaction(transactionUpdate);
// Update cache if transaction was successful
if (result.committed && result.snapshot?.exists()) {
await this.cacheOfflineData(path, result.snapshot.val());
}
return result;
} catch (error) {
console.error('Transaction failed:', error);
throw new Error(`Transaction failed: ${error.message}`);
}
}
// Batch Operations
async batchUpdate(updates: { [path: string]: any }): Promise<void> {
try {
const timestampedUpdates: { [path: string]: any } = {};
for (const [path, data] of Object.entries(updates)) {
timestampedUpdates[path] = {
...data,
updatedAt: serverTimestamp(),
};
}
await update(this.database, timestampedUpdates);
// Update cache
for (const [path, data] of Object.entries(timestampedUpdates)) {
await this.cacheOfflineData(path, data);
}
} catch (error) {
console.error('Batch update failed:', error);
throw new Error(`Batch update failed: ${error.message}`);
}
}
// Advanced Operations
async searchByField(
path: string,
fieldName: string,
searchValue: any
): Promise<RealtimeData[]> {
return this.queryData(path, {
orderBy: 'child',
orderByChild: fieldName,
equalTo: searchValue,
});
}
async paginateResults(
path: string,
pageSize: number,
startAfter?: string,
orderByField?: string
): Promise<RealtimeData[]> {
const options: QueryOptions = {
limitToFirst: pageSize + 1, // Get one extra to check if there are more pages
};
if (orderByField) {
options.orderBy = 'child';
options.orderByChild = orderByField;
} else {
options.orderBy = 'key';
}
if (startAfter) {
options.startAfter = startAfter;
}
const results = await this.queryData(path, options);
// Remove the extra item if it exists
if (results.length > pageSize) {
return results.slice(0, pageSize);
}
return results;
}
// Utility Methods
getReference(path: string): DatabaseReference {
return this.database.child(path);
}
async exists(path: string): Promise<boolean> {
try {
const snapshot = await get(this.database.child(path));
return snapshot.exists();
} catch (error) {
console.error('Failed to check existence:', error);
return false;
}
}
async getTimestamp(): Promise<number> {
// Get server timestamp
const timestampRef = push(this.database, { timestamp: serverTimestamp() });
const snapshot = await get(timestampRef);
return snapshot.val().timestamp;
}
generatePushKey(): string {
return push(this.database).key || '';
}
// Cache Management
clearCache(): void {
this.offlineCache.clear();
}
async clearOfflineStorage(): Promise<void> {
try {
this.clearCache();
const keys = await AsyncStorage.getAllKeys();
const dbKeys = keys.filter(key => key.startsWith('db_cache_'));
await AsyncStorage.multiRemove(dbKeys);
} catch (error) {
console.error('Failed to clear offline storage:', error);
}
}
async syncOfflineChanges(): Promise<void> {
try {
// This would sync any offline changes with the server
// Implementation depends on your specific offline sync strategy
console.log('Syncing offline changes...');
} catch (error) {
console.error('Failed to sync offline changes:', error);
}
}
}
export default RealtimeDatabaseService;
// src/services/ChatService.ts
import RealtimeDatabaseService, { DatabaseNode } from './RealtimeDatabaseService';
export interface Message {
id: string;
text: string;
senderId: string;
senderName: string;
timestamp: number;
type: 'text' | 'image' | 'file';
imageUrl?: string;
fileName?: string;
fileSize?: number;
readBy: string[];
replyTo?: string;
reactions?: { [emoji: string]: string[] };
}
export interface ChatRoom {
id: string;
name: string;
description?: string;
participants: string[];
createdAt: number;
lastMessage?: Message;
unreadCount: { [userId: string]: number };
typing: { [userId: string]: boolean };
}
export interface User {
id: string;
name: string;
email: string;
avatar?: string;
isOnline: boolean;
lastSeen: number;
}
class ChatService {
private static instance: ChatService;
private db: RealtimeDatabaseService;
private currentUserId: string;
constructor(userId: string) {
this.currentUserId = userId;
this.db = RealtimeDatabaseService.getInstance();
}
static getInstance(userId: string): ChatService {
if (!ChatService.instance || ChatService.instance['currentUserId'] !== userId) {
ChatService.instance = new ChatService(userId);
}
return ChatService.instance;
}
// Chat Room Management
async createChatRoom(
name: string,
participants: string[],
description?: string
): Promise<string> {
const chatRoom: Omit<ChatRoom, 'id'> = {
name,
description,
participants: [...participants, this.currentUserId],
createdAt: Date.now(),
unreadCount: {},
typing: {},
};
const roomId = await this.db.createData('chatRooms', chatRoom);
// Initialize unread counts
for (const participant of chatRoom.participants) {
await this.db.updateData(`chatRooms/${roomId}/unreadCount/${participant}`, 0);
}
return roomId;
}
async getChatRoom(roomId: string): Promise<ChatRoom | null> {
return await this.db.readData(`chatRooms/${roomId}`);
}
async getUserChatRooms(): Promise<ChatRoom[]> {
const userRooms = await this.db.searchByField(
'chatRooms',
'participants',
this.currentUserId
);
return userRooms.map(room => ({ ...room.value, id: room.key }));
}
async updateChatRoom(roomId: string, updates: Partial<ChatRoom>): Promise<void> {
await this.db.updateData(`chatRooms/${roomId}`, updates);
}
async leaveChatRoom(roomId: string): Promise<void> {
const room = await this.getChatRoom(roomId);
if (room) {
const updatedParticipants = room.participants.filter(id => id !== this.currentUserId);
await this.db.updateData(`chatRooms/${roomId}`, {
participants: updatedParticipants,
});
}
}
// Message Management
async sendMessage(
roomId: string,
text: string,
type: 'text' | 'image' | 'file' = 'text',
options?: {
imageUrl?: string;
fileName?: string;
fileSize?: number;
replyTo?: string;
}
): Promise<string> {
const message: Omit<Message, 'id'> = {
text,
senderId: this.currentUserId,
senderName: await this.getUserDisplayName(this.currentUserId),
timestamp: Date.now(),
type,
imageUrl: options?.imageUrl,
fileName: options?.fileName,
fileSize: options?.fileSize,
replyTo: options?.replyTo,
readBy: [this.currentUserId],
reactions: {},
};
const messageId = await this.db.createData(`messages/${roomId}`, message);
// Update chat room last message
const room = await this.getChatRoom(roomId);
if (room) {
await this.db.updateData(`chatRooms/${roomId}`, {
lastMessage: { ...message, id: messageId },
});
// Increment unread counts for other participants
for (const participant of room.participants) {
if (participant !== this.currentUserId) {
await this.runTransaction(`chatRooms/${roomId}/unreadCount/${participant}`, (currentCount) => {
return (currentCount || 0) + 1;
});
}
}
}
return messageId;
}
async editMessage(roomId: string, messageId: string, newText: string): Promise<void> {
await this.db.updateData(`messages/${roomId}/${messageId}`, {
text: newText,
edited: true,
editedAt: Date.now(),
});
}
async deleteMessage(roomId: string, messageId: string): Promise<void> {
await this.db.deleteData(`messages/${roomId}/${messageId}`);
}
async markMessageAsRead(roomId: string, messageId: string): Promise<void> {
await this.db.runTransaction(`messages/${roomId}/${messageId}/readBy`, (readBy) => {
const currentReadBy = readBy || [];
if (!currentReadBy.includes(this.currentUserId)) {
return [...currentReadBy, this.currentUserId];
}
return currentReadBy;
});
}
async markAllMessagesAsRead(roomId: string): Promise<void> {
// Reset unread count
await this.db.setData(`chatRooms/${roomId}/unreadCount/${this.currentUserId}`, 0);
}
// Typing Indicators
async setTyping(roomId: string, isTyping: boolean): Promise<void> {
await this.db.setData(`chatRooms/${roomId}/typing/${this.currentUserId}`, isTyping);
// Auto-remove typing indicator after 3 seconds
if (isTyping) {
setTimeout(async () => {
await this.db.setData(`chatRooms/${roomId}/typing/${this.currentUserId}`, false);
}, 3000);
}
}
// Reactions
async addReaction(roomId: string, messageId: string, emoji: string): Promise<void> {
await this.db.runTransaction(`messages/${roomId}/${messageId}/reactions/${emoji}`, (currentReactors) => {
const reactors = currentReactors || [];
if (!reactors.includes(this.currentUserId)) {
return [...reactors, this.currentUserId];
}
return reactors;
});
}
async removeReaction(roomId: string, messageId: string, emoji: string): Promise<void> {
await this.db.runTransaction(`messages/${roomId}/${messageId}/reactions/${emoji}`, (currentReactors) => {
const reactors = currentReactors || [];
return reactors.filter((id: string) => id !== this.currentUserId);
});
}
// Message Listeners
onNewMessage(roomId: string, callback: (message: Message) => void): () => void {
return this.db.onChildAdded(`messages/${roomId}`, (node) => {
const message: Message = {
...node.value,
id: node.key,
};
callback(message);
});
}
onMessageUpdated(roomId: string, callback: (message: Message) => void): () => void {
return this.db.onChildChanged(`messages/${roomId}`, (node) => {
const message: Message = {
...node.value,
id: node.key,
};
callback(message);
});
}
onMessageDeleted(roomId: string, callback: (messageId: string) => void): () => void {
return this.db.onChildRemoved(`messages/${roomId}`, (node) => {
callback(node.key);
});
}
onTypingUsers(roomId: string, callback: (typingUsers: string[]) => void): () => void {
return this.db.onValueChange(`chatRooms/${roomId}/typing`, (typingData) => {
if (typingData) {
const typingUsers = Object.entries(typingData)
.filter(([userId, isTyping]) => isTyping && userId !== this.currentUserId)
.map(([userId]) => userId);
callback(typingUsers);
}
});
}
// User Management
async getUserProfile(userId: string): Promise<User | null> {
return await this.db.readData(`users/${userId}`);
}
async updateUserProfile(updates: Partial<User>): Promise<void> {
await this.db.updateData(`users/${this.currentUserId}`, updates);
}
async setUserOnline(isOnline: boolean): Promise<void> {
await this.db.updateData(`users/${this.currentUserId}`, {
isOnline,
lastSeen: Date.now(),
});
}
async getUserDisplayName(userId: string): Promise<string> {
const user = await this.getUserProfile(userId);
return user?.name || 'Unknown User';
}
onUserOnlineStatus(userId: string, callback: (isOnline: boolean) => void): () => void {
return this.db.onValueChange(`users/${userId}/isOnline`, callback);
}
// Utility Methods
private async runTransaction(path: string, updateFunction: (currentData: any) => any): Promise<void> {
await this.db.runTransaction(path, updateFunction);
}
async searchMessages(roomId: string, searchTerm: string): Promise<Message[]> {
// This is a simplified search - in production, you might want to use
// a dedicated search service like Algolia or Elasticsearch
const messages = await this.db.queryData(`messages/${roomId}`, {
orderBy: 'key',
});
return messages
.map(msg => ({ ...msg.value, id: msg.key }))
.filter(msg =>
msg.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
msg.senderName.toLowerCase().includes(searchTerm.toLowerCase())
);
}
async getMessageHistory(roomId: string, limit: number = 50, startAfter?: string): Promise<Message[]> {
const messages = await this.db.paginateResults(
`messages/${roomId}`,
limit,
startAfter,
'timestamp'
);
return messages.map(msg => ({ ...msg.value, id: msg.key }));
}
async getUnreadCount(roomId: string): Promise<number> {
const unreadCount = await this.db.readData(`chatRooms/${roomId}/unreadCount/${this.currentUserId}`);
return unreadCount || 0;
}
async getTotalUnreadCount(): Promise<number> {
const rooms = await this.getUserChatRooms();
let total = 0;
for (const room of rooms) {
const unreadCount = await this.getUnreadCount(room.id);
total += unreadCount;
}
return total;
}
}
export default ChatService;
💻 Intégration Cloud Functions React Native Firebase typescript
Implémenter une logique backend serverless avec Firebase Cloud Functions incluant les triggers, les tâches planifiées et les endpoints API personnalisés
// Firebase Cloud Functions (Node.js - functions/index.js)
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const express = require('express');
const cors = require('cors');
const sgMail = require('@sendgrid/mail');
const Stripe = require('stripe');
const sharp = require('sharp');
const path = require('path');
// Initialize Firebase Admin
admin.initializeApp();
// Initialize external services
const stripe = Stripe(functions.config().stripe.secret_key);
sgMail.setApiKey(functions.config().sendgrid.api_key);
// Express app for HTTP functions
const app = express();
app.use(cors({ origin: true }));
// Cloud Function: Send Email Notification
exports.sendEmailNotification = functions.https.onCall(async (data, context) => {
// Check if user is authenticated
if (!context.auth) {
throw new functions.https.HttpsError(
'unauthenticated',
'The function must be called while authenticated.'
);
}
const { to, subject, text, html, templateData } = data;
try {
const msg = {
to,
from: '[email protected]',
subject,
text,
html,
templateId: templateData?.templateId,
dynamicTemplateData: templateData?.data,
};
await sgMail.send(msg);
return { success: true, message: 'Email sent successfully' };
} catch (error) {
console.error('Email sending error:', error);
throw new functions.https.HttpsError(
'internal',
'Failed to send email'
);
}
});
// Cloud Function: Process Payment with Stripe
exports.processPayment = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'Authentication required');
}
const { amount, currency = 'usd', paymentMethodId, description } = data;
try {
// Create a payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency,
payment_method: paymentMethodId,
confirmation_method: 'manual',
confirm: true,
description,
metadata: {
userId: context.auth.uid,
},
});
// Record payment in Firestore
const paymentRecord = {
userId: context.auth.uid,
stripePaymentId: paymentIntent.id,
amount,
currency,
status: paymentIntent.status,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
};
await admin.firestore().collection('payments').doc(paymentIntent.id).set(paymentRecord);
return {
success: true,
clientSecret: paymentIntent.client_secret,
status: paymentIntent.status,
};
} catch (error) {
console.error('Payment processing error:', error);
throw new functions.https.HttpsError('internal', 'Payment processing failed');
}
});
// Cloud Function: Generate Thumbnail
exports.generateThumbnail = functions.storage.object().onFinalize(async (object) => {
const fileBucket = object.bucket;
const filePath = object.name;
const contentType = object.contentType;
// Exit if this is triggered on a file that is not an image.
if (!contentType.startsWith('image/')) {
return console.log('This is not an image.');
}
// Exit if the image is already a thumbnail.
if (filePath.includes('thumbnail_')) {
return console.log('Already a thumbnail.');
}
// Download file from bucket.
const bucket = admin.storage().bucket(fileBucket);
const tempFilePath = path.join(os.tmpdir(), filePath);
const thumbnailFilePath = path.join(path.dirname(filePath), `thumbnail_${path.basename(filePath)}`);
await bucket.file(filePath).download({
destination: tempFilePath,
});
// Generate a thumbnail using Sharp.
const thumbnailBuffer = await sharp(tempFilePath)
.resize(200, 200, {
fit: 'inside',
withoutEnlargement: true,
})
.toBuffer();
// Upload thumbnail to Storage.
const thumbnailFile = bucket.file(thumbnailFilePath);
await thumbnailFile.save(thumbnailBuffer, {
metadata: {
contentType: contentType,
},
});
// Clean up the temporary file.
return fs.unlinkSync(tempFilePath);
});
// Cloud Function: User Activity Analytics
exports.trackUserActivity = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'Authentication required');
}
const { action, properties = {} } = data;
const userId = context.auth.uid;
try {
const activity = {
userId,
action,
properties,
timestamp: admin.firestore.FieldValue.serverTimestamp(),
userAgent: context.rawRequest.headers['user-agent'],
ip: context.rawRequest.ip,
};
await admin.firestore().collection('userActivity').add(activity);
// Update user analytics
const userRef = admin.firestore().collection('users').doc(userId);
await userRef.update({
lastActivity: admin.firestore.FieldValue.serverTimestamp(),
totalActivities: admin.firestore.FieldValue.increment(1),
});
return { success: true };
} catch (error) {
console.error('Activity tracking error:', error);
throw new functions.https.HttpsError('internal', 'Failed to track activity');
}
});
// Cloud Function: Send Push Notification
exports.sendPushNotification = functions.https.onCall(async (data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'Authentication required');
}
const { userIds, title, body, data: notificationData, imageUrl } = data;
try {
// Get user FCM tokens
const userDocs = await admin.firestore()
.collection('users')
.where(admin.firestore.FieldPath.documentId(), 'in', userIds)
.get();
const tokens = [];
userDocs.forEach(doc => {
const userData = doc.data();
if (userData.fcmToken) {
tokens.push(userData.fcmToken);
}
});
if (tokens.length === 0) {
return { success: false, message: 'No valid FCM tokens found' };
}
const message = {
notification: {
title,
body,
...(imageUrl && { imageUrl }),
},
data: notificationData || {},
tokens,
};
const response = await admin.messaging().sendMulticast(message);
// Handle failed tokens
if (response.failureCount > 0) {
const failedTokens = [];
response.responses.forEach((resp, idx) => {
if (!resp.success) {
failedTokens.push(tokens[idx]);
}
});
// Remove invalid tokens from user documents
for (const token of failedTokens) {
await admin.firestore()
.collection('users')
.where('fcmToken', '==', token)
.limit(1)
.get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
doc.ref.update({ fcmToken: admin.firestore.FieldValue.delete() });
});
});
}
}
return {
success: true,
successCount: response.successCount,
failureCount: response.failureCount,
};
} catch (error) {
console.error('Push notification error:', error);
throw new functions.https.HttpsError('internal', 'Failed to send push notification');
}
});
// Scheduled Cloud Function: Daily Digest
exports.sendDailyDigest = functions.pubsub
.schedule('0 9 * * *') // Every day at 9 AM
.timeZone('America/New_York')
.onRun(async (context) => {
try {
// Get all users who opted in for daily digest
const usersSnapshot = await admin.firestore()
.collection('users')
.where('dailyDigest', '==', true)
.get();
const batchPromises = [];
usersSnapshot.forEach(async (doc) => {
const user = doc.data();
// Get user's activity from last 24 hours
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const activitySnapshot = await admin.firestore()
.collection('userActivity')
.where('userId', '==', doc.id)
.where('timestamp', '>=', yesterday)
.get();
if (activitySnapshot.empty) {
return; // Skip if no activity
}
// Generate digest content
const activities = activitySnapshot.docs.map(doc => doc.data());
const digestContent = generateDigestContent(activities);
// Send email digest
const msg = {
to: user.email,
from: '[email protected]',
subject: 'Your Daily Digest',
html: digestContent,
};
batchPromises.push(sgMail.send(msg));
});
await Promise.all(batchPromises);
console.log('Daily digest sent successfully');
return null;
} catch (error) {
console.error('Daily digest error:', error);
return null;
}
});
// Realtime Database Trigger: User Presence
exports.updateUserPresence = functions.database
.ref('/users/{userId}/online')
.onUpdate(async (change, context) => {
const { userId } = context.params;
const isOnline = change.after.val();
if (isOnline) {
// User came online
await admin.firestore().collection('users').doc(userId).update({
online: true,
lastSeen: admin.firestore.FieldValue.serverTimestamp(),
});
} else {
// User went offline
await admin.firestore().collection('users').doc(userId).update({
online: false,
lastSeen: admin.firestore.FieldValue.serverTimestamp(),
});
}
});
// Firestore Trigger: User Welcome
exports.sendWelcomeEmail = functions.firestore
.document('users/{userId}')
.onCreate(async (snapshot, context) => {
const user = snapshot.data();
if (user.email) {
const msg = {
to: user.email,
from: '[email protected]',
subject: 'Welcome to Our App!',
templateId: 'd-welcome-template',
dynamicTemplateData: {
name: user.displayName || 'User',
},
};
try {
await sgMail.send(msg);
console.log('Welcome email sent to:', user.email);
} catch (error) {
console.error('Welcome email error:', error);
}
}
});
// Helper function to generate digest content
function generateDigestContent(activities) {
const groupedActivities = activities.reduce((acc, activity) => {
const action = activity.action;
if (!acc[action]) {
acc[action] = [];
}
acc[action].push(activity);
return acc;
}, {});
let html = '<h2>Your Daily Activity Summary</h2>';
for (const [action, items] of Object.entries(groupedActivities)) {
html += `<h3>${action.charAt(0).toUpperCase() + action.slice(1)} (${items.length})</h3>`;
html += '<ul>';
items.forEach(item => {
const time = new Date(item.timestamp.toDate()).toLocaleTimeString();
html += `<li>${time} - ${item.properties.description || 'No description'}</li>`;
});
html += '</ul>';
}
return html;
}
// src/services/CloudFunctionsService.ts
import { https } from '@react-native-firebase/functions';
import { Alert } from 'react-native';
export interface CloudFunctionResponse<T = any> {
success: boolean;
data?: T;
error?: string;
}
export interface EmailNotificationData {
to: string;
subject: string;
text?: string;
html?: string;
templateData?: {
templateId: string;
data: Record<string, any>;
};
}
export interface PaymentData {
amount: number;
currency?: string;
paymentMethodId: string;
description?: string;
}
export interface UserActivityData {
action: string;
properties?: Record<string, any>;
}
export interface PushNotificationData {
userIds: string[];
title: string;
body: string;
data?: Record<string, any>;
imageUrl?: string;
}
class CloudFunctionsService {
private static instance: CloudFunctionsService;
static getInstance(): CloudFunctionsService {
if (!CloudFunctionsService.instance) {
CloudFunctionsService.instance = new CloudFunctionsService();
}
return CloudFunctionsService.instance;
}
private async callFunction<T>(functionName: string, data: any): Promise<CloudFunctionResponse<T>> {
try {
const result = await https().call(functionName, data);
return {
success: true,
data: result.data,
};
} catch (error) {
console.error(`Cloud function error (${functionName}):`, error);
let errorMessage = 'An unknown error occurred';
if (error.details) {
errorMessage = error.details;
} else if (error.message) {
errorMessage = error.message;
}
return {
success: false,
error: errorMessage,
};
}
}
// Email Notifications
async sendEmailNotification(data: EmailNotificationData): Promise<CloudFunctionResponse> {
return await this.callFunction('sendEmailNotification', data);
}
// Payment Processing
async processPayment(data: PaymentData): Promise<CloudFunctionResponse> {
const response = await this.callFunction('processPayment', data);
if (!response.success) {
Alert.alert('Payment Error', response.error || 'Payment processing failed');
}
return response;
}
// User Activity Tracking
async trackUserActivity(data: UserActivityData): Promise<CloudFunctionResponse> {
return await this.callFunction('trackUserActivity', data);
}
// Push Notifications
async sendPushNotification(data: PushNotificationData): Promise<CloudFunctionResponse> {
return await this.callFunction('sendPushNotification', data);
}
// Custom Functions
async callCustomFunction<T>(functionName: string, data: any): Promise<CloudFunctionResponse<T>> {
return await this.callFunction<T>(functionName, data);
}
// Utility Methods
async sendWelcomeEmail(email: string, displayName: string): Promise<CloudFunctionResponse> {
return await this.sendEmailNotification({
to: email,
subject: 'Welcome to Our App!',
templateData: {
templateId: 'd-welcome-template',
data: { name: displayName || 'User' },
},
});
}
async sendPasswordResetEmail(email: string, resetLink: string): Promise<CloudFunctionResponse> {
return await this.sendEmailNotification({
to: email,
subject: 'Password Reset Request',
html: `
<h2>Password Reset</h2>
<p>You requested to reset your password. Click the link below to reset it:</p>
<a href="${resetLink}">Reset Password</a>
<p>If you didn't request this, please ignore this email.</p>
`,
});
}
async sendOrderConfirmation(email: string, orderDetails: any): Promise<CloudFunctionResponse> {
return await this.sendEmailNotification({
to: email,
subject: 'Order Confirmation',
templateData: {
templateId: 'd-order-confirmation',
data: orderDetails,
},
});
}
async trackPageView(pageName: string, properties?: Record<string, any>): Promise<void> {
await this.trackUserActivity({
action: 'page_view',
properties: {
page: pageName,
...properties,
},
});
}
async trackPurchase(productId: string, amount: number, currency: string): Promise<void> {
await this.trackUserActivity({
action: 'purchase',
properties: {
productId,
amount,
currency,
timestamp: Date.now(),
},
});
}
async trackFeatureUsage(featureName: string, properties?: Record<string, any>): Promise<void> {
await this.trackUserActivity({
action: 'feature_usage',
properties: {
feature: featureName,
...properties,
},
});
}
}
export default CloudFunctionsService;
// src/services/AnalyticsService.ts
import analytics from '@react-native-firebase/analytics';
import CloudFunctionsService from './CloudFunctionsService';
export interface AnalyticsEvent {
name: string;
parameters?: Record<string, any>;
}
export interface UserProperties {
[key: string]: string | number | boolean;
}
class AnalyticsService {
private static instance: AnalyticsService;
private cloudFunctions: CloudFunctionsService;
private constructor() {
this.cloudFunctions = CloudFunctionsService.getInstance();
}
static getInstance(): AnalyticsService {
if (!AnalyticsService.instance) {
AnalyticsService.instance = new AnalyticsService();
}
return AnalyticsService.instance;
}
// Basic Analytics
async logEvent(name: string, parameters?: Record<string, any>): Promise<void> {
try {
await analytics().logEvent(name, parameters);
// Also send to custom analytics if needed
await this.cloudFunctions.trackUserActivity({
action: 'analytics_event',
properties: {
eventName: name,
parameters,
},
});
} catch (error) {
console.error('Analytics logging error:', error);
}
}
async setUserId(userId: string): Promise<void> {
try {
await analytics().setUserId(userId);
} catch (error) {
console.error('Set user ID error:', error);
}
}
async setUserProperties(properties: UserProperties): Promise<void> {
try {
await analytics().setUserProperties(properties);
} catch (error) {
console.error('Set user properties error:', error);
}
}
// Standard E-commerce Events
async logPurchase(transactionId: string, value: number, currency: string, item?: any): Promise<void> {
const parameters: any = {
transaction_id: transactionId,
value,
currency,
};
if (item) {
parameters.items = [item];
}
await this.logEvent('purchase', parameters);
}
async logViewItem(itemId: string, itemName: string, itemCategory: string, price?: number): Promise<void> {
await this.logEvent('view_item', {
item_id: itemId,
item_name: itemName,
item_category: itemCategory,
value: price,
});
}
async logAddToCart(itemId: string, itemName: string, itemCategory: string, quantity: number, price: number): Promise<void> {
await this.logEvent('add_to_cart', {
item_id: itemId,
item_name: itemName,
item_category: itemCategory,
quantity,
value: price * quantity,
});
}
async logBeginCheckout(items: any[], value: number, currency: string): Promise<void> {
await this.logEvent('begin_checkout', {
items,
value,
currency,
});
}
// App Events
async logAppOpen(): Promise<void> {
await this.logEvent('app_open');
}
async logScreenView(screenName: string, screenClass?: string): Promise<void> {
await analytics().logScreenView({
screen_name: screenName,
screen_class: screenClass || screenName,
});
}
async logSearch(searchTerm: string): Promise<void> {
await this.logEvent('search', {
search_term: searchTerm,
});
}
async logShare(contentType: string, itemId: string): Promise<void> {
await this.logEvent('share', {
content_type: contentType,
item_id: itemId,
});
}
async logSignUp(method: string): Promise<void> {
await this.logEvent('sign_up', {
method,
});
}
async logLogin(method: string): Promise<void> {
await this.logEvent('login', {
method,
});
}
// Custom Events
async trackCustomEvent(eventName: string, parameters?: Record<string, any>): Promise<void> {
await this.logEvent(eventName, parameters);
}
// Session Tracking
async setSessionTimeoutDuration(duration: number): Promise<void> {
try {
await analytics().setAnalyticsCollectionEnabled(true);
// Note: Session timeout duration setting might not be available in React Native
// This is typically configured in Firebase console
} catch (error) {
console.error('Session timeout error:', error);
}
}
async enableAnalytics(): Promise<void> {
try {
await analytics().setAnalyticsCollectionEnabled(true);
} catch (error) {
console.error('Enable analytics error:', error);
}
}
async disableAnalytics(): Promise<void> {
try {
await analytics().setAnalyticsCollectionEnabled(false);
} catch (error) {
console.error('Disable analytics error:', error);
}
}
// Error Tracking
async trackError(error: Error, context?: Record<string, any>): Promise<void> {
await this.logEvent('app_error', {
error_message: error.message,
error_stack: error.stack,
context,
});
}
}
export default AnalyticsService;