Skip to content

feat(functions): turbo modules implementation #8603

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

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3dfaa1c
feat(storage): move to turbomodules
russellwheatley Jul 2, 2025
9d896d2
chore: convert to TS
russellwheatley Jul 2, 2025
b80b1a7
format
russellwheatley Jul 2, 2025
1431fbf
chore: ignore some things for now
russellwheatley Jul 2, 2025
cff1c5f
chore: update package.json main & types
russellwheatley Jul 2, 2025
abd0073
init with bob builder
russellwheatley Jul 2, 2025
9f30fa8
yarn.lock
russellwheatley Jul 2, 2025
18c0fdf
chore: rm rootDir from tsconfig.json
russellwheatley Jul 2, 2025
800bf1a
test: import as type for tests
russellwheatley Jul 2, 2025
1962777
chore: fix types
russellwheatley Jul 2, 2025
4dd309f
update types
russellwheatley Jul 2, 2025
193aa9b
rm type test
russellwheatley Jul 2, 2025
a49cf27
chore: fix path of type definitions
russellwheatley Jul 2, 2025
6b52129
chore: moving functions to TS
russellwheatley Jul 2, 2025
1214b3e
fix: imports
russellwheatley Jul 3, 2025
34138fa
fix some types
russellwheatley Jul 3, 2025
6802209
update types
russellwheatley Jul 3, 2025
a1cd58a
port over remaining types and delete js files
russellwheatley Jul 3, 2025
bd27a05
remove functions declaration
russellwheatley Jul 3, 2025
3b645b1
chore: create spec for turbomodules
russellwheatley Jul 3, 2025
909b963
rename spec
russellwheatley Jul 3, 2025
98f387c
rm old version of cli
russellwheatley Jul 7, 2025
7bad518
fix: need react-native as dependency for codegen to work
russellwheatley Jul 7, 2025
3ba3a76
update scripts for generating code
russellwheatley Jul 7, 2025
d3ca750
generated code for android & ios
russellwheatley Jul 7, 2025
7eda482
update podspec & config file as per RN docs
russellwheatley Jul 7, 2025
e714ad8
rm generated code
russellwheatley Jul 7, 2025
9f17910
rename to Native prefix otherwise ignored by codegen
russellwheatley Jul 7, 2025
d8ffb33
updated generic types for spec
russellwheatley Jul 7, 2025
4bd4607
latest generated code based on updated spec
russellwheatley Jul 7, 2025
90bbbed
update naming of generated code
russellwheatley Jul 7, 2025
938d1c3
update RNFB functions module
russellwheatley Jul 7, 2025
d4b2541
make file `.mm` & update podspec for functions
russellwheatley Jul 8, 2025
fba772e
podfile.lock
russellwheatley Jul 8, 2025
88eeda8
Merge branch 'main' into functions-turbo
russellwheatley Jul 8, 2025
3218d4d
yarn.lock
russellwheatley Jul 8, 2025
30f5d35
chore: update version
russellwheatley Jul 8, 2025
cdfee94
chore: reverse order of prepare script
russellwheatley Jul 8, 2025
a04eb09
feat(android): make NativeFunctionsModule compatible with existing code
russellwheatley Jul 8, 2025
1ca5cbb
update path
russellwheatley Jul 9, 2025
1ca1da2
move modular/namespaced to same directory to fix TS
russellwheatley Jul 9, 2025
f4014d0
exclude jni from compilation
russellwheatley Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"lerna:clean": "lerna clean",
"build:all:clean": "lerna run build:clean",
"build:all:build": "lerna run build",
"codegen:all": "lerna run codegen",
"lint": "yarn lint:js && yarn lint:android && yarn lint:ios:check",
"lint:js": "eslint packages/* --max-warnings=0",
"lint:android": "(google-java-format --set-exit-if-changed --replace --glob=\"packages/*/android/src/**/*.java\" || (echo \"\n\nandroid formatting error - please re-run\n\n\" && exit 1)) && (git diff --exit-code packages/*/android/src || (echo \"\n\nandroid files changed from linting, please examine and commit result\n\n\" && exit 1))",
Expand Down Expand Up @@ -69,6 +70,8 @@
"@firebase/rules-unit-testing": "^4.0.1",
"@inquirer/prompts": "^7.4.1",
"@octokit/core": "^6.1.5",
"@react-native-community/cli": "latest",
"@react-native/codegen": "^0.80.1",
"@tsconfig/node-lts": "^22.0.1",
"@types/react": "^19.0.0",
"@types/react-native": "^0.73.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class UniversalFirebaseModule {
private final Context context;
private final String serviceName;

protected UniversalFirebaseModule(Context context, String serviceName) {
public UniversalFirebaseModule(Context context, String serviceName) {
this.context = context;
this.serviceName = serviceName;
this.executorService = new TaskExecutorService(getName());
Expand All @@ -43,7 +43,7 @@ public Context getApplicationContext() {
return getContext().getApplicationContext();
}

protected ExecutorService getExecutor() {
public ExecutorService getExecutor() {
return executorService.getExecutor();
}

Expand Down
10 changes: 9 additions & 1 deletion packages/functions/RNFBFunctions.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,19 @@ Pod::Spec.new do |s|
s.ios.deployment_target = firebase_ios_target
s.macos.deployment_target = firebase_macos_target
s.tvos.deployment_target = firebase_tvos_target
s.source_files = 'ios/**/*.{h,m}'
s.source_files = 'ios/**/*.{h,m,mm,cpp}'
s.exclude_files = 'ios/generated/RCTThirdPartyComponentsProvider.*', 'ios/generated/RCTAppDependencyProvider.*', 'ios/generated/RCTModuleProviders.*', 'ios/generated/RCTModulesConformingToProtocolsProvider.*', 'ios/generated/RCTUnstableModulesRequiringMainQueueSetupProvider.*'

s.compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -DFOLLY_CFG_NO_COROUTINES=1'

# React Native dependencies
s.dependency 'React-Core'
s.dependency 'RNFBApp'
s.dependency 'ReactCodegen'
s.dependency 'ReactAppDependencyProvider'
# TODO - not sure if we need these two as I believe they're UI related
s.dependency 'React-Fabric'
s.dependency 'RCT-Folly'

if defined?($FirebaseSDKVersion)
Pod::UI.puts "#{s.name}: Using user specified Firebase SDK version '#{$FirebaseSDKVersion}'"
Expand Down
5 changes: 3 additions & 2 deletions packages/functions/__tests__/functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it, jest } from '@jest/globals';

import functions, {
import {
firebase,
getFunctions,
connectFunctionsEmulator,
Expand All @@ -9,9 +9,10 @@ import functions, {
HttpsErrorCode,
} from '../lib';

import functions from '../lib/namespaced';
import {
createCheckV9Deprecation,
CheckV9DeprecationFunction,
type CheckV9DeprecationFunction,
} from '../../app/lib/common/unitTestUtils';

// @ts-ignore test
Expand Down
4 changes: 3 additions & 1 deletion packages/functions/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,11 @@ android {
}
}

sourceSets {
sourceSets {
main {
java.srcDirs = ['src/main/java', 'src/reactnative/java']
// Exclude generated JNI files from compilation
java.excludes = ['**/generated/jni/**']
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package io.invertase.firebase.functions;

/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import static io.invertase.firebase.functions.UniversalFirebaseFunctionsModule.CODE_KEY;
import static io.invertase.firebase.functions.UniversalFirebaseFunctionsModule.DATA_KEY;
import static io.invertase.firebase.functions.UniversalFirebaseFunctionsModule.DETAILS_KEY;
import static io.invertase.firebase.functions.UniversalFirebaseFunctionsModule.MSG_KEY;

import com.facebook.fbreact.specs.NativeFunctionsModuleSpec;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.google.android.gms.tasks.Task;
import com.google.firebase.FirebaseApp;
import com.google.firebase.functions.FirebaseFunctionsException;
import io.invertase.firebase.common.RCTConvertFirebase;
import io.invertase.firebase.common.ReactNativeFirebaseModule;
import io.invertase.firebase.common.UniversalFirebaseModule;
;
import java.io.IOException;

public class NativeFunctionsModule extends NativeFunctionsModuleSpec {
private static final String SERVICE_NAME = "Functions";
private final UniversalFirebaseFunctionsModule module;
private final UniversalFirebaseModule universalFirebaseModule;

public NativeFunctionsModule(ReactApplicationContext reactContext) {
super(reactContext);
// cannot have multiple inheritance so we make this a property rather than extending it
universalFirebaseModule = new UniversalFirebaseModule(reactContext, SERVICE_NAME);
this.module = new UniversalFirebaseFunctionsModule(reactContext, SERVICE_NAME);
}

@Override
public void httpsCallable(
String emulatorHost,
double emulatorPort,
String name,
ReadableMap data,
ReadableMap options,
Promise promise) {

// Get the default Firebase app
FirebaseApp firebaseApp = FirebaseApp.getInstance();
String region = "us-central1"; // Default region

// Extract data from the wrapper
Object callableData = data.toHashMap().get(DATA_KEY);

// Convert emulatorPort to Integer (null if not using emulator)
Integer port = emulatorHost != null ? (int) emulatorPort : null;

Task<Object> callMethodTask = module.httpsCallable(
firebaseApp.getName(), region, emulatorHost, port, name, callableData, options);

// resolve
callMethodTask.addOnSuccessListener(
universalFirebaseModule.getExecutor(),
result -> {
promise.resolve(RCTConvertFirebase.mapPutValue(DATA_KEY, result, Arguments.createMap()));
});

// reject
callMethodTask.addOnFailureListener(
universalFirebaseModule.getExecutor(),
exception -> {
Object details = null;
String code = "UNKNOWN";
String message = exception.getMessage();
WritableMap userInfo = Arguments.createMap();
if (exception.getCause() instanceof FirebaseFunctionsException) {
FirebaseFunctionsException functionsException =
(FirebaseFunctionsException) exception.getCause();
details = functionsException.getDetails();
code = functionsException.getCode().name();
message = functionsException.getMessage();
String timeout = FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name();
Boolean isTimeout = code.contains(timeout);

if (functionsException.getCause() instanceof IOException && !isTimeout) {
// return UNAVAILABLE for network io errors, to match iOS
code = FirebaseFunctionsException.Code.UNAVAILABLE.name();
message = FirebaseFunctionsException.Code.UNAVAILABLE.name();
}
}
RCTConvertFirebase.mapPutValue(CODE_KEY, code, userInfo);
RCTConvertFirebase.mapPutValue(MSG_KEY, message, userInfo);
RCTConvertFirebase.mapPutValue(DETAILS_KEY, details, userInfo);
promise.reject(code, message, exception, userInfo);
});
}

@Override
public void httpsCallableFromUrl(
String emulatorHost,
double emulatorPort,
String url,
ReadableMap data,
ReadableMap options,
Promise promise) {

// Get the default Firebase app
FirebaseApp firebaseApp = FirebaseApp.getInstance();
String region = "us-central1"; // Default region

// Extract data from the wrapper
Object callableData = data.toHashMap().get(DATA_KEY);

// Convert emulatorPort to Integer (null if not using emulator)
Integer port = emulatorHost != null ? (int) emulatorPort : null;

Task<Object> callMethodTask = module.httpsCallableFromUrl(
firebaseApp.getName(), region, emulatorHost, port, url, callableData, options);

// resolve
callMethodTask.addOnSuccessListener(
universalFirebaseModule.getExecutor(),
result -> {
promise.resolve(RCTConvertFirebase.mapPutValue(DATA_KEY, result, Arguments.createMap()));
});

// reject
callMethodTask.addOnFailureListener(
universalFirebaseModule.getExecutor(),
exception -> {
Object details = null;
String code = "UNKNOWN";
String message = exception.getMessage();
WritableMap userInfo = Arguments.createMap();
if (exception.getCause() instanceof FirebaseFunctionsException) {
FirebaseFunctionsException functionsException =
(FirebaseFunctionsException) exception.getCause();
details = functionsException.getDetails();
code = functionsException.getCode().name();
message = functionsException.getMessage();
String timeout = FirebaseFunctionsException.Code.DEADLINE_EXCEEDED.name();
Boolean isTimeout = code.contains(timeout);

if (functionsException.getCause() instanceof IOException && !isTimeout) {
// return UNAVAILABLE for network io errors, to match iOS
code = FirebaseFunctionsException.Code.UNAVAILABLE.name();
message = FirebaseFunctionsException.Code.UNAVAILABLE.name();
}
}
RCTConvertFirebase.mapPutValue(CODE_KEY, code, userInfo);
RCTConvertFirebase.mapPutValue(MSG_KEY, message, userInfo);
RCTConvertFirebase.mapPutValue(DETAILS_KEY, details, userInfo);
promise.reject(code, message, exception, userInfo);
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.invertase.firebase.functions;

/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nonnull;

@SuppressWarnings("unused")
public class NativeFunctionsPackage implements ReactPackage {
@Nonnull
@Override
public List<NativeModule> createNativeModules(@Nonnull ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new NativeFunctionsModule(reactContext));
return modules;
}

@Nonnull
@Override
public List<ViewManager> createViewManagers(@Nonnull ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GenerateModuleJavaSpec.js
*
* @nolint
*/

package com.facebook.fbreact.specs;

import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public abstract class NativeFunctionsModuleSpec extends ReactContextBaseJavaModule implements TurboModule {
public static final String NAME = "NativeFunctionsModule";

public NativeFunctionsModuleSpec(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public @Nonnull String getName() {
return NAME;
}

@ReactMethod
@DoNotStrip
public abstract void httpsCallable(@Nullable String emulatorHost, double emulatorPort, String name, ReadableMap data, ReadableMap options, Promise promise);

@ReactMethod
@DoNotStrip
public abstract void httpsCallableFromUrl(@Nullable String emulatorHost, double emulatorPort, String url, ReadableMap data, ReadableMap options, Promise promise);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

file(GLOB react_codegen_SRCS CONFIGURE_DEPENDS *.cpp react/renderer/components/NativeFunctionsModule/*.cpp)

add_library(
react_codegen_NativeFunctionsModule
OBJECT
${react_codegen_SRCS}
)

target_include_directories(react_codegen_NativeFunctionsModule PUBLIC . react/renderer/components/NativeFunctionsModule)

target_link_libraries(
react_codegen_NativeFunctionsModule
fbjni
jsi
# We need to link different libraries based on whether we are building rncore or not, that's necessary
# because we want to break a circular dependency between react_codegen_rncore and reactnative
reactnative
)

target_compile_options(
react_codegen_NativeFunctionsModule
PRIVATE
-DLOG_TAG=\"ReactNative\"
-fexceptions
-frtti
-std=c++20
-Wall
)
Loading