Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Build] Separate firebase-enabled and non-firebase builds #143

Open
a-ghorbani opened this issue Dec 21, 2024 · 9 comments
Open

[Build] Separate firebase-enabled and non-firebase builds #143

a-ghorbani opened this issue Dec 21, 2024 · 9 comments

Comments

@a-ghorbani
Copy link
Owner

Issue description
Currently the app only needs Firebase (App Check) for one specific feature: sharing benchmark results with the community. We use App Check to verify the legitimacy of these benchmark submissions (i.e., to ensure they come from real users rather than automated scripts, etc). For custom compiles or other platforms like fdroid we can disable sharing benchmarks, so Firebase is unnecessary.

Therefore, we want to provide two distinct build variants/flavors:

  • Firebase-Enabled – Includes Firebase libraries (App Check, etc.) and allows sharing benchmark results.
  • Non-Firebase – Excludes all Firebase references and cannot share benchmark results (or just greys out that feature).

Potential solution:

Android

  • Define product flavors in android/app/build.gradle:
    flavorDimensions "default"
    productFlavors {
      firebaseEnabled {
        dimension "default"
        // This flavor includes Firebase dependencies (App Check) 
        // for sharing benchmark results and verifying legitimacy
      }
      firebaseDisabled {
        dimension "default"
        // This flavor has no Firebase libraries -> cannot share benchmarks
      }
    }
  • In the firebaseEnabled flavor:
    • Add google-services.json and apply the Google Services plugin.
  • In the firebaseDisabled flavor:
    • Omit google-services.json.
    • Do not include the Firebase dependencies.

iOS

  • we can create two targets / schemes in Xcode:
    • FirebaseEnabled target references GoogleService-Info.plist.
    • FirebaseDisabled target does not include any Firebase refs.
  • conditionally exclude / include Firebase in Podfile depending on build configs.

ts code

  • Optionally separate or conditionally import the @react-native-firebase modules (e.g., app, app-check)
  • Or rely on autolinking + native flavor settings
@a-ghorbani a-ghorbani added the question Further information is requested label Dec 21, 2024
@a-ghorbani a-ghorbani changed the title Separate Firebase-Enabled and Non-Firebase Builds Separate firebase-enabled and non-firebase builds Dec 21, 2024
@a-ghorbani a-ghorbani changed the title Separate firebase-enabled and non-firebase builds [Build] Separate firebase-enabled and non-firebase builds Dec 21, 2024
@MahmoudMabrok
Copy link
Contributor

what about calling firebase through api, I think it has RESTFull API we can use instead of adding the library itself.

As example

@a-ghorbani
Copy link
Owner Author

It seems the api is for the db not app check? The app is using fb for the App Check.

@TFWol
Copy link

TFWol commented Jan 8, 2025

I was just about to ask about the sudden web traffic I saw coming from the appstore app, as soon as it launched, that wasn't there before.

@a-ghorbani
Copy link
Owner Author

a-ghorbani commented Jan 8, 2025

I was just about to ask about the sudden web traffic I saw coming from the appstore app, as soon as it launched, that wasn't there before.

Yeah, I don’t like being dependent on Google services, but I don’t have many options for allowing folks to share their benchmarks for this: https://huggingface.co/spaces/a-ghorbani/ai-phone-leaderboard. I am just a single dev :)

If anyone has an alternative that doesn’t require using Firebase App Check for this, I’d be happy to explore it.

By the way, what tool do you use for monitoring network traffic?

@TFWol
Copy link

TFWol commented Jan 8, 2025

Is it possible to make it only connect to firebase when an attempt to share a Benchmark occurs?
Right now it connects each time the application is opened.

For looking at traffic, I use a combination of things and always switching them around, but currently:

Also, I tend to proxy iphone through stuff like Fiddler (Classic)

@TFWol
Copy link

TFWol commented Jan 25, 2025

btw, I don't know about coding on iOS, but I used an LLM for ideas. Does this make any sense or seem feasible?


Analyzing the Current Implementation

Your current AppDelegate.mm file initializes Firebase and App Check in the didFinishLaunchingWithOptions method. This causes Firebase to connect every time the app launches. Let's modify this to implement lazy initialization.

Step-by-Step Modification of AppDelegate.mm

  1. Create a Singleton Manager Class

First, let's create a new Objective-C class called FirebaseManager. This will handle the lazy initialization of Firebase.

Create a new file named FirebaseManager.h:

// FirebaseManager.h
#import <Foundation/Foundation.h>

@interface FirebaseManager : NSObject

+ (instancetype)sharedInstance;
- (void)configureFirebaseIfNeeded;

@end

Now create FirebaseManager.m:

// FirebaseManager.m
#import "FirebaseManager.h"
#import <Firebase.h>
#import "RNFBAppCheckModule.h"

@implementation FirebaseManager

static FirebaseManager *sharedInstance = nil;
static dispatch_once_t onceToken;

+ (instancetype)sharedInstance {
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

- (void)configureFirebaseIfNeeded {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [RNFBAppCheckModule sharedInstance];
        [FIRApp configure];
        NSLog(@"Firebase configured");
    });
}

@end
  1. Modify AppDelegate.mm

Now, let's update your AppDelegate.mm file:

#import "AppDelegate.h"
#import "FirebaseManager.h"
#import <React/RCTBundleURLProvider.h>
#import <RNFSBackgroundDownloads.h>

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Remove Firebase initialization from here
    self.moduleName = @"PocketPal";
    self.initialProps = @{};
    return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

// ... (rest of your AppDelegate implementation)

@end
  1. Expose Firebase Configuration to JavaScript

To allow your React Native code to trigger Firebase initialization when needed, create a native module:

Create a new file named RNFirebaseManager.h:

// RNFirebaseManager.h
#import <React/RCTBridgeModule.h>

@interface RNFirebaseManager : NSObject <RCTBridgeModule>
@end

Now create RNFirebaseManager.m:

// RNFirebaseManager.m
#import "RNFirebaseManager.h"
#import "FirebaseManager.h"

@implementation RNFirebaseManager

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(configureFirebase)
{
    [[FirebaseManager sharedInstance] configureFirebaseIfNeeded];
}

@end
  1. Use in React Native Code

In your React Native JavaScript code, you can now control when Firebase is initialized:

import { NativeModules } from 'react-native';

const { RNFirebaseManager } = NativeModules;

// Call this when you need Firebase
function initializeFirebaseIfNeeded() {
  RNFirebaseManager.configureFirebase();
}

// Use this function before any Firebase-dependent operations

Explanation of Changes

  • We've removed the Firebase initialization from AppDelegate.mm.
  • A new FirebaseManager singleton class now handles Firebase configuration.
  • The configureFirebaseIfNeeded method ensures Firebase is only configured once, even if called multiple times.
  • We've exposed a method to React Native to trigger Firebase initialization when needed.

Best Practices and Considerations

  1. Timing: Call initializeFirebaseIfNeeded() in your JavaScript code before any Firebase-dependent operations.
  2. Error Handling: Add appropriate error handling in the native module and JavaScript code.
  3. Testing: Thoroughly test to ensure all Firebase-dependent features still work correctly with this lazy loading approach.
  4. Performance Monitoring: Use Xcode Instruments or Firebase Performance Monitoring to verify the impact of these changes.

By implementing these changes, your app will no longer automatically connect to firebaseappcheck.googleapis.com on launch. Instead, the connection will only be established when you explicitly initialize Firebase through your React Native code.

@TFWol
Copy link

TFWol commented Jan 25, 2025

Then I tried Copilot's Output:


To stop the app from reaching out to firebaseappcheck.googleapis.com on launch and instead do the check only when using the Benchmark feature, you can make the following changes:

  1. Modify the App Check Initialization:

    • Move the initializeAppCheck function call from the app launch to the Benchmark feature.
  2. Update the Benchmark Code:

    • Ensure the initializeAppCheck and getAppCheckToken functions are called within the Benchmark feature before submitting the benchmark data.

Here are the relevant code snippets:

import '@react-native-firebase/app-check';
import firebase from '@react-native-firebase/app';
import { APPCHECK_DEBUG_TOKEN_ANDROID, APPCHECK_DEBUG_TOKEN_IOS } from '@env';

let isAppCheckInitialized = false;

export const initializeAppCheck = () => {
  if (isAppCheckInitialized) {
    return;
  }
  try {
    const rnfbProvider = firebase
      .appCheck()
      .newReactNativeFirebaseAppCheckProvider();

    rnfbProvider.configure({
      android: {
        provider: __DEV__ ? 'debug' : 'playIntegrity',
        debugToken: APPCHECK_DEBUG_TOKEN_ANDROID,
      },
      apple: {
        provider: __DEV__ ? 'debug' : 'appAttestWithDeviceCheckFallback',
        debugToken: APPCHECK_DEBUG_TOKEN_IOS,
      },
    });
    firebase.appCheck().initializeAppCheck({
      provider: rnfbProvider,
      isTokenAutoRefreshEnabled: true,
    });

    isAppCheckInitialized = true;
  } catch (error) {
    console.error('Failed to initialize Firebase App Check:', error);
  }
};

export const getAppCheckToken = async () => {
  try {
    if (!firebase.appCheck) {
      throw new Error('Firebase App Check module is not available');
    }
    const { token } = await firebase.appCheck().getToken(true);
    return token;
  } catch (error) {
    console.error('Failed to get App Check token:', error);
    throw error;
  }
};
import axios from 'axios';
import { urls } from '../config';
import { getAppCheckToken, initializeAppCheck } from '../utils/fb';
import { BenchmarkResult, DeviceInfo } from '../utils/types';

type SubmissionData = {
  deviceInfo: DeviceInfo;
  benchmarkResult: BenchmarkResult;
};

export async function submitBenchmark(
  deviceInfo: DeviceInfo,
  benchmarkResult: BenchmarkResult,
): Promise<{ message: string; id: number }> {
  try {
    // Initialize App Check here
    initializeAppCheck();
    const appCheckToken = await getAppCheckToken();

    if (!appCheckToken) {
      throw new Error('Failed to obtain App Check token');
    }

    const data: SubmissionData = {
      deviceInfo,
      benchmarkResult,
    };

    const response = await axios.post(urls.benchmarkSubmit(), data, {
      headers: {
        'X-Firebase-AppCheck': appCheckToken,
        'Content-Type': 'application/json',
      },
    });
    return response.data;
  } catch (error) {
    console.error('Error submitting benchmark:', error);
    if (error instanceof Error) {
      console.error('Error details:', error.message);
    }
    throw error;
  }
}
const handleShareResult = async (result: BenchmarkResult) => {
  if (!deviceInfo) {
    throw new Error('Device information not available');
  }
  if (result.submitted) {
    throw new Error('This benchmark has already been submitted');
  }
  try {
    // Initialize App Check here
    initializeAppCheck();
    const response = await submitBenchmark(deviceInfo, result);
    console.log('Benchmark submitted successfully:', response);
    benchmarkStore.markAsSubmitted(result.uuid);
  } catch (error) {
    console.error('Failed to submit benchmark:', error);
    throw error;
  }
};

By following these steps, you can ensure that the Firebase App Check is only performed when the Benchmark feature is used. For more details, you can view the search results.

@TFWol
Copy link

TFWol commented Jan 30, 2025

@a-ghorbani ...thoughts?

@a-ghorbani
Copy link
Owner Author

@TFWol This is very helpful. Thanks for digging into this. by the looks of it, it seems like it should work. and will make it easier to create separate builds too. I'll be divin into this, but need a bit time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants