Skip to content

Instantly share code, notes, and snippets.

@Yiannistaos
Last active May 23, 2024 10:15
Show Gist options
  • Save Yiannistaos/68d5790168ae1f77a57980aa03b5048e to your computer and use it in GitHub Desktop.
Save Yiannistaos/68d5790168ae1f77a57980aa03b5048e to your computer and use it in GitHub Desktop.
Integrating Firebase with React Native

Integrating Firebase with React Native

Step-by-Step Guide to Integrating Firebase with Your React Native Application

Investigate is in 90%. It will be finished in the first week of November 2023.

Live CMS (This demo has been made for demonstration purposes only)

What is Firebase?

Firebase is a development platform from Google that offers a wide range of tools and services for building web and mobile applications. It provides developers with a collection of tools and infrastructure to help them develop high-quality apps, grow their user base, and earn revenue.

Firebase Cloud Messaging (FCM) (pros and cons)

Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages at no cost.

Pros:

  1. Cross-Platform: FCM supports messaging on iOS, Android, and web. This makes it easier for developers to integrate notifications across multiple platforms.

  2. Cost: FCM is free, regardless of the volume of messages, making it a cost-effective solution for sending notifications.

  3. High Throughput: It can send a large number of push notifications at once, ensuring quick delivery to users.

  4. Multimedia Messaging: Apart from text, FCM supports sending images, sounds, and other multimedia messages.

  5. Topic Subscription: Users can subscribe to specific topics, allowing developers to send targeted notifications based on interests or other criteria.

  6. Upstream Messaging: Not just from server to client, FCM also supports messages from the client back to the server.

  7. Message Scheduling and Prioritization: Messages can be set with different priorities, ensuring that important notifications are delivered promptly.

  8. Offline Handling: If the device is offline, FCM stores the message and delivers it when the device comes back online.

  9. Integration with Firebase Suite: Being part of the Firebase suite, it integrates well with other Firebase products like Firebase Analytics, making it easier to analyze the impact of your messages.

  10. Rich Client SDKs: SDKs are provided for popular platforms, simplifying integration and usage.

Cons:

  1. Limitations in Payload Size: There's a limitation in the size of the payload you can send. For instance, notification messages sent from the app server can be up to 4KB and data messages can be up to 4KB for iOS and 4KB for Android.

  2. Reliability: While FCM is generally reliable, no messaging platform can guarantee 100% delivery rates, especially considering device-related issues (like battery optimizations that kill background processes).

  3. Complexity in iOS Integration: To use FCM for iOS notifications, you still need to integrate with Apple Push Notification Service (APNs), adding an extra layer of complexity.

  4. Data Privacy Concerns: Using Google services sometimes raises data privacy concerns. Depending on your target market or user base, this could be a potential drawback.

  5. Limited Customization for Rich Notifications: For more advanced and richly formatted notifications, additional work might be required beyond basic FCM capabilities.

  6. Dependency: Relying on a third-party service means if FCM experiences any downtime or issues, it could affect your application's notification delivery.

  7. Migration Challenges: If you're moving from another notification service, migrating to FCM might involve some challenges, especially if the existing system has features not present in FCM.

Cloud Firestore (pros and cons)

Cloud Firestore is a flexible, scalable NoSQL cloud database for storing and syncing data for client- and server-side development.

Pros:

  1. Real-time Updates: Firestore supports real-time data synchronization. This means that any changes to the data in the database are immediately reflected in the app without the need for manual refreshing.

  2. Scalability: Firestore is built to scale automatically, which means that it can handle large amounts of data and high numbers of users without requiring manual intervention or reconfiguration.

  3. Flexibility: Being a NoSQL database, Firestore allows for flexible data structures. Schemas can evolve and change as the app grows.

  4. Offline Support: Firestore caches data for offline use. This ensures that React Native apps remain functional even when the user's device isn't connected to the internet.

  5. Integrated Authentication: Firestore is integrated with Firebase Authentication, making it relatively straightforward to set up user authentication.

  6. Rich Querying: Firestore supports rich queries that allow you to filter, sort, and fetch data efficiently.

  7. Security: Firestore's security rules allow fine-grained control over who can access data and what operations they can perform.

  8. Cross-platform: Firestore works with multiple platforms, including iOS, Android, and web. This is particularly useful for React Native developers who are targeting multiple platforms.

  9. Serverless: With Firestore, you can build apps without needing to manage server infrastructure. Cloud Functions for Firebase allows for server-side logic.

  10. SDKs and Libraries: Google provides SDKs for different languages and platforms, and there are various community libraries to simplify integration with React Native, like "react-native-firebase".

Cons:

  1. Complex Pricing Model: While Firestore has a generous free tier, its pricing is based on the number of reads, writes, and deletes. It can be challenging to estimate costs, especially for apps with unpredictable usage patterns.

  2. Cold Start Times: If using Cloud Functions alongside Firestore, you might experience latency issues due to "cold starts", where a function hasn't been invoked recently and takes longer to respond.

  3. Learning Curve: The real-time nature, security rules, and querying capabilities of Firestore can be overwhelming for developers who are new to the platform.

  4. Data Modeling Challenges: NoSQL databases like Firestore require a different approach to data modeling compared to relational databases. This might involve data duplication and denormalization to optimize read operations.

  5. Query Limitations: Firestore has certain limitations in its querying capabilities. For example, it doesn't support native OR queries or JOIN operations.

  6. Migration: Migrating data in or out of Firestore can be challenging, especially for large datasets.

  7. Size Limitations: Documents in Firestore are limited to 1MB in size. This requires careful structuring of data, especially for apps with large records.

In conclusion, while Firestore offers many features that can accelerate the development of React Native apps, developers should be aware of its limitations and pricing model. It's essential to evaluate the specific requirements of your application and decide whether Firestore is the right fit.

Firebase Pricing

Firebase Cloud Messaging (FCM)

Is free of charge, regardless of the volume of messages you send. So, if an admin sends a push notification to 100 devices, the cost for this is $0.

Firestore Cost for Storing the Message

Assuming an admin sends a push notification via Firebase Cloud Messaging to X devices:

1 Upon receipt, devices process the notification and store the message in Firestore. 2) When users click the notification or open the app, the stored message is displayed. 3) Users have the option to delete individual or all messages from the "messages" collection in Firestore.

Let's break down the actions:

  • Document writes (If a device handles the push notification and the message is stored to Firestore, you are performing X writes, since there are X devices.)
  • Document reads (When the user clicks on the notification or opens the app, and the notification is displayed on the screen, you are performing a read operation. So, you'll be billed for X reads.)
  • Document deletes (If the users are able to delete the message or all messages, you're performing a delete operation. Each delete is also billed, but it's billed the same as a write operation. So, if all X users delete their respective messages, you'll be billed for X deletes.)
  • Storage (There's also the cost of storing the messages in Firestore, but this is generally minimal unless you're storing a significant volume of data.)

A Real Example with Pricing Calculator:

For 50,000 app installs (5,000 Daily Active Users)

  1. 400K total daily reads 50K No-cost reads + (350K reads at $0.06/100K)= 3.5 * $0.06 = $0.21 / day * 30 = $6.30
  2. 100K total daily writes: 20K No-cost writes + (80K writes at $0.18/100K) = 0.8 * $0.18 = $0.14 / day * 30 = $4.20
  3. 100K total daily deletes: 20K No-cost deletes + (80K deletes at $0.02/100K) = 0.8 * $0.02 = $0.02 / day * 30 = $0.60

Total cost: $12.14/month

Resources

Messages via Firebase Console vs External Notification System

Case 1 - Messages via Firebase Console

An admin sends messages to users through the Firebase Console. Upon receiving these push notifications, devices handle and process these messages, subsequently storing them in Firestore for later display when users log in.

Pros:

  1. Direct Integration: Utilizes Firebase's built-in capabilities, ensuring smooth compatibility.
  2. Simplicity: Sending messages directly through the Firebase Console is straightforward, reducing the need for additional tools or platforms.
  3. Real-time Feedback: The Firebase Console provides real-time metrics and feedback on the delivery and interaction rates of the sent notifications.

Cons:

  1. Limited Customization: Firebase Console might not offer as much flexibility as bespoke or third-party solutions in terms of message formatting, scheduling, or advanced analytics.
  2. Dependency: Relying solely on Firebase Console could pose risks if there are outages or disruptions in the Firebase service.
  3. Device Processing: Devices are required to handle and process the notification, which can lead to slightly higher power and data usage on the user's end.
  4. Notification Dependencies: If users have disabled notifications and the app relies on notification payloads (rather than data payloads) for processing and saving messages to Firestore, there's a risk that the messages won't be processed, especially if the app is not in the foreground. This could lead to missed messages in Firestore.

enter image description here

Case 2 - External Notification System

An external notification system directly stores messages in Firestore. Following this storage, push notifications are sent to users. When users access the app, messages are directly retrieved from Firestore, bypassing any need for the device to process the push notification's content.

Pros:

  1. Expandability: An external system might offer more advanced features, customization options, or integration capabilities with other platforms.
  2. Optimized Retrieval: Since messages are directly fetched from Firestore, there's no intermediate processing required, leading to potentially faster message display times.

Cons:

  1. Complexity: Integrating an external system may introduce additional layers of complexity, both in terms of setup and maintenance.
  2. Cost Implications: Using an external system may introduce additional costs or require dedicated resources for setup, integration, and maintenance.

enter image description here

Getting Stared

A. Create the Project to the Firebase Console

  1. Create the project via Firebase Console. The name of the project (e.g. com.anytime) will be used internally and in your app's Firebase SDK setup.
  2. Once your project is ready, click on the gear icon ⚙️ on the top left corner, then select "Project settings" to see details about your project or to integrate Firebase services into your app.
  3. Click on the appropriate icon for your platform (iOS, Android, Web, etc.) in the Firebase Console. Follow the setup steps, which include adding your app's bundle ID and downloading the Firebase configuration file.
  4. Depending on your platform, you'll be given instructions on how to integrate the Firebase SDK into your app.

Remember, once the project is created, you can always add or modify Firebase services, view analytics, set up authentication, databases, and other services, directly from the Firebase Console.

Screenshot from Firebase Console

enter image description here

B. Install Dependencies

The Dependencies
  1. @react-native-firebase/app: Core module for integrating Firebase with React Native apps.
  2. @react-native-firebase/firestore: Firebase Cloud Firestore integration for React Native.
  3. @react-native-firebase/messaging: Firebase Cloud Messaging (push notifications) for React Native.
  4. react-native-permissions: Manage and request user permissions for React Native apps.
Using npm

npm install @react-native-firebase/app@^18.5.0 @react-native-firebase/firestore@^18.5.0 @react-native-firebase/messaging@^18.5.0 react-native-permissions@^3.9.3

Using yarn

yarn add @react-native-firebase/app@^18.5.0 @react-native-firebase/firestore@^18.5.0 @react-native-firebase/messaging@^18.5.0 react-native-permissions@^3.9.3

Packages in package.json
"dependencies": {
    "@react-native-firebase/app": "^18.5.0",
    "@react-native-firebase/firestore": "^18.5.0",
    "@react-native-firebase/messaging": "^18.5.0",
    "react-native-permissions": "^3.9.3",
    ... other dependencies
}

C. Android Setup

Step 1 - google-services.json

Download the file google-services.json from Firebase Console as you can see in the screenshot below, and move it here /android/app/google-services.json

enter image description here

Step 2 - android/app/build.gradle

File: /android/app/build.gradle

A. At the top:

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

B. Inside the dependencies:

dependencies {	
	// ... existing code
	implementation 'com.google.firebase:firebase-messaging:23.2.1'
}

Step 3 - android/build.gradle

File: /android/build.gradle

Inside the dependencies:

dependencies {	
	// ... existing code
	classpath  'com.google.gms:google-services:4.3.8'
}

Step 4 - AndroidManifest.xml

File: /android/app/src/main/AndroidManifest.xml

A. Add the user permissions:

	<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
	<uses-permission android:name="android.permission.VIBRATE" />

B. Inside <application>:

		// ... existing code
		<service android:name="io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService">
			<intent-filter>
				<action  android:name="com.google.firebase.MESSAGING_EVENT"  />
			</intent-filter>
		</service>
	</application>
</manifest>

D. iOS - Setup

Step 1 - GoogleService-Info.plist

Download the file GoogleService-Info.plist from Firebase Console as you can see in the screenshot below, and move it here /ios/Anytime/GoogleService-Info.plist

iOS firebase Console

Step 2 - AppDelegate.mm

File: /ios/Anytime/AppDelegate.mm

A. Add the top:

#import <Firebase.h>

B. In the middle of thefile, add the line [FIRApp configure]; as you can see below:

- (BOOL)application:(UIApplication *)application  didFinishLaunchingWithOptions:(NSDictionary  *)launchOptions
{
	// add this line below
	[FIRApp configure];
	
	// ...other code

Step 3 - Podfile

File: /ios/Podfile

A. At the top of the file add this line:

use_modular_headers!

B. After flags = get_default_flags() add the pod 'gRPC-Core' , as you can see in the code below.

target 'Anytime'  do
	config = use_native_modules!
	
	# Flags change depending on the env values.
	flags = get_default_flags()

	pod 'gRPC-Core' // <<< add this line

C. Comment the line :flipper_configuration => flipper_config,, as you can see in the code below.

	# you should disable the next line.
    # :flipper_configuration => flipper_config, // <<< comment this line
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."

D. Handle incoming background messages and saves them to Firestore. /index.js

/index.tsx

import  firestore  from  '@react-native-firebase/firestore';
import  messaging  from  '@react-native-firebase/messaging';

/**
 * Handles incoming background messages and saves them to Firestore.
 * @param {Object} remoteMessage - The remote message received.
 */
messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log(
    'Message handled in the background!',
    JSON.stringify(remoteMessage)
  );

  const { body, title } = remoteMessage.notification;
  const { data } = remoteMessage;
  const sendTime = firestore.Timestamp.fromMillis(remoteMessage.sentTime);
  const fcmToken = await messaging().getToken();

  // Save the remote message details to Firestore
  firestore()
    .collection('remoteMessages')
    .add({
      body,
      title,
      sendTime,
      data,
      deviceToken: fcmToken
    })
    .then(() => {
      console.log('Remote message saved!');
    })
    .catch(error => {
      console.error('Error saving remote message: ', error);
    });
});

// Register the app component with the app name
AppRegistry.registerComponent(appName, () => App);

E. Screen with Remote Messages

/src/ui/screens/FireTestsScreen/index.tsx

import React, { useState, useEffect } from 'react';
import firestore from '@react-native-firebase/firestore';
import messaging from '@react-native-firebase/messaging';
import {
  View,
  StyleSheet,
  Text,
  FlatList,
  Platform,
  PermissionsAndroid,
  RefreshControl
} from 'react-native';

import { SafeAreaContainer } from '../../components/common';

// Define types for the state and any other custom data structures.
interface Device {
  id: string;
  // Add other attributes if necessary.
}

interface RemoteMessage {
  id: string;
  title: string;
  body: string;
  sendTime: firestore.Timestamp;
  // Add other attributes if necessary.
}

if (Platform.OS === 'android') {
  PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS);
}

const FireTestsScreen: React.FC = () => {
  const [devices, setDevices] = useState<Device[]>([]);
  const [remoteMessages, setRemoteMessages] = useState<RemoteMessage[]>([]);
  const [refreshing, setRefreshing] = useState<boolean>(false);

  const fetchDevices = async () => {
    try {
      const querySnapshot = await firestore().collection('userDevices').get();
      const devicesList = querySnapshot.docs.map(documentSnapshot => {
        return {
          ...documentSnapshot.data(),
          id: documentSnapshot.id
        };
      });
      setDevices(devicesList);
    } catch (error) {
      console.error('Error fetching devices:', error);
    }
  };

  const fetchRemoteMessages = async fcmToken => {
    try {
      const snapshot = await firestore()
        .collection('remoteMessages')
        .where('deviceToken', '==', fcmToken)
        .orderBy('sendTime', 'desc') // Order by sendTime in descending order
        .get();

      const messagesList = snapshot.docs.map(doc => ({
        ...doc.data(),
        id: doc.id
      }));

      setRemoteMessages(messagesList);
    } catch (error) {
      console.error('Error fetching remote messages:', error);
    }
  };

  const handleRefresh = async (): Promise<void> => {
    setRefreshing(true);
    const fcmToken: string | null = await messaging().getToken();
    await fetchRemoteMessages(fcmToken); // Fetch remoteMessages on pull to refresh
    setRefreshing(false);
  };

  useEffect(() => {
    // Get the device token
    messaging()
      .requestPermission()
      .then(() => {
        console.log('Permission granted');
      })
      .catch(error => {
        console.error('Error requesting permission: ', error);
      });
  }, []);

  useEffect(() => {
    const fetchDevicesAsync = async () => {
      try {
        await fetchDevices(); // Fetch devices initially
      } catch (error) {
        console.error('Error fetching devices:', error);
      }
    };
    fetchDevicesAsync();
  }, []);

  useEffect(() => {
    const unsubscribe = messaging().onMessage(async remoteMessage => {
      console.log('A new FCM message arrived!', JSON.stringify(remoteMessage));
      const { body, title } = remoteMessage.notification;
      const { data } = remoteMessage;
      const sendTime = firestore.Timestamp.fromMillis(remoteMessage.sentTime);
      const fcmToken = await messaging().getToken();

      // Save the remote message details to Firestore
      firestore()
        .collection('remoteMessages')
        .add({
          body,
          title,
          sendTime,
          data,
          deviceToken: fcmToken
        })
        .then(() => {
          console.log('Remote message saved!');
          // Reload the remote messages after saving the new one
          fetchRemoteMessages(fcmToken);
        })
        .catch(error => {
          console.error('Error saving remote message: ', error);
        });
    });

    return unsubscribe;
  }, []);

  useEffect(() => {
    async function setupNotifications() {
      const authStatus = await messaging().requestPermission();
      console.log(
        'messaging.AuthorizationStatus',
        messaging.AuthorizationStatus
      );
      const enabled =
        authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
        authStatus === messaging.AuthorizationStatus.PROVISIONAL;

      if (enabled) {
        console.log('Authorization status:', authStatus);
        const fcmToken = await messaging().getToken();
        console.log('edw fcmToken', Platform.OS, fcmToken);

        fetchRemoteMessages(fcmToken); // Fetch remoteMessages here
        const devicePlatform = Platform.OS;
        const added_at = firestore.Timestamp.now();

        // Check if device is already registered in Firestore.
        const existingDevice = await firestore()
          .collection('userDevices')
          .where('deviceToken', '==', fcmToken)
          .get();

        if (existingDevice.empty) {
          // Add new device to Firestore.
          firestore()
            .collection('userDevices')
            .add({
              deviceToken: fcmToken,
              devicePlatform,
              added_at
            })
            .then(() => {
              console.log('Device added!');
              fetchDevices(); // Fetch devices again after a new device is added
            })
            .catch(error => {
              console.error('Error adding device: ', error);
            });
        }
      }
    }

    setupNotifications();
  }, []); // Empty dependency array ensures this runs only once when the component mounts.

  return (
    <SafeAreaContainer>
      <View style={styles.container}>
        <Text>Remote Messages for Current Device:</Text>
        <FlatList
          data={remoteMessages}
          renderItem={({ item }) => (
            <View style={{ marginBottom: 10 }}>
              <Text>ID: {item.id}</Text>
              <Text>Title: {item.title}</Text>
              <Text>Body: {item.body}</Text>
              <Text>Send Time: {item.sendTime.toDate().toString()}</Text>
              {/* Render data key-values here, if required */}
            </View>
          )}
          keyExtractor={item => item.id}
          refreshing={refreshing}
          onRefresh={handleRefresh}
          refreshControl={
            <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
          }
        />
      </View>
    </SafeAreaContainer>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  }
});

export default FireTestsScreen;

F. Firestore Rules

Allow only body, title, sendTime, data and deviceToken. The fields body and title are required.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

	match /{document=**} {
      allow read: if true;
    }

    match /remoteMessages/{messageId} {
      allow write: if request.resource.data.keys().hasOnly(['body', 'title', 'sendTime', 'data', 'deviceToken'])
                   && 'body' in request.resource.data
                   && 'title' in request.resource.data;
    }
  }
}

Other Examples with Firestore

Get deviceToken from vatNumber

const vatNumberToSearch = "EL123456789";

db.collection('anytimeAppUsers')
  .where('vatNumber', '==', vatNumberToSearch)
  .get()
  .then(snapshot => {
      if (snapshot.empty) {
          console.log('No matching documents.');
          return;
      }

      snapshot.forEach(doc => {
          console.log('Device Token:', doc.data().deviceToken);
      });
  })
  .catch(err => {
      console.error('Error fetching documents:', err);
  });
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment