🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
React Native Firebase Integrationsbeispiele
React Native Firebase Integrationsbeispiele einschließlich Authentifizierung, Echtzeit-Datenbank, Cloud Functions, Push-Benachrichtigungen, Analytics und Cloud Storage
💻 React Native Firebase Authentifizierung typescript
🟢 simple
Umfassende Firebase Authentifizierung implementieren inklusive E-Mail/Passwort, Social Login, biometrischer Authentifizierung und sicherer Session-Verwaltung
// 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;
💻 React Native Firebase Echtzeit-Datenbank typescript
🟡 intermediate
Kollaborative Echtzeit-Anwendungen mit Firebase Realtime Database erstellen inklusive Offline-Unterstützung, Datensynchronisation und komplexen Abfragen
// 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;
💻 React Native Firebase Cloud Functions Integration typescript
🔴 complex
Serverless Backend-Logik mit Firebase Cloud Functions implementieren inklusive Triggern, geplanten Aufgaben und benutzerdefinierten API-Endpunkten
// 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;