diff --git a/.github/workflows/scripts/boot-simulator.sh b/.github/workflows/scripts/boot-simulator.sh
index 739956df07..fceb97dd48 100755
--- a/.github/workflows/scripts/boot-simulator.sh
+++ b/.github/workflows/scripts/boot-simulator.sh
@@ -42,6 +42,51 @@ describe_booted_device() {
|| true
}
+resolve_device_udid() {
+ local device="$1"
+ local udid
+
+ udid="$(
+ xcrun simctl list devices booted 2>/dev/null \
+ | grep -F "${device} (" \
+ | grep -v 'unavailable' \
+ | head -1 \
+ | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' \
+ || true
+ )"
+ if [[ -n "$udid" ]]; then
+ echo "$udid"
+ return 0
+ fi
+
+ udid="$(
+ xcrun simctl list devices available 2>/dev/null \
+ | grep -F "${device} (" \
+ | grep -v 'unavailable' \
+ | head -1 \
+ | sed -E 's/.*\(([A-F0-9-]+)\).*/\1/' \
+ || true
+ )"
+ echo "$udid"
+}
+
+kill_resolved_simulator() {
+ local device="$1"
+ local udid
+
+ udid="$(resolve_device_udid "$device")"
+ if [[ -z "$udid" ]]; then
+ log_boot_status "phase=kill_resolved device=\"${device}\" not found, skipping"
+ return 0
+ fi
+
+ log_boot_status "phase=kill_resolved udid=${udid} device=\"${device}\""
+ killall Simulator 2>/dev/null || true
+ xcrun simctl terminate "$udid" com.invertase.testing 2>/dev/null || true
+ xcrun simctl shutdown "$udid" 2>/dev/null || true
+ xcrun simctl shutdown "$device" 2>/dev/null || true
+}
+
log_migration_status() {
local device="$1"
local migration_output probe_rc
@@ -117,10 +162,8 @@ popd >/dev/null || exit 1
log_boot_status "phase=resolve_device name=\"${SIM}\" (from tests/.detoxrc.js)"
-# Clear up any existing attempts in case we are re-trying
-log_boot_status "phase=shutdown_existing killing Simulator.app if running..."
-killall Simulator 2>/dev/null || true
-xcrun simctl shutdown "$SIM" 2>/dev/null || true
+# Kill the resolved simulator first when present (CI pre-boot and e2e Jet retries).
+kill_resolved_simulator "$SIM"
log_boot_status "phase=boot_command starting simctl boot..."
set +e
diff --git a/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch b/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch
index 4bccb154b5..5763cd934a 100644
--- a/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch
+++ b/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch
@@ -1,8 +1,54 @@
diff --git a/dist/Server.js b/dist/Server.js
-index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b4eaf1bba 100644
+index ad9debe2086ab9b96e97a69aec966da8114ad102..c8f4e2a1b9d3f6e8a7c5d4b2e1f0a9c8b7d6e5f4 100644
--- a/dist/Server.js
+++ b/dist/Server.js
-@@ -130,6 +130,17 @@ class Server extends ServerEventEmitter_1.ServerEventEmitter {
+@@ -73,6 +73,8 @@
+ /** The options to send to the next connecting running client */
+ this.clientOptions = {};
+ this._listening = false;
++ this._clientResetTimer = null;
++ this._awaitingInitialClientRun = false;
+ this.handleConnection = (ws, req) => {
+ this.debug("Client connected");
+ // Check that the protocol matches
+@@ -88,6 +90,11 @@
+ ws.close(1002, `Expected "${expectedProtocol}" protocol got "${ws.protocol}"`);
+ return;
+ }
++ if (this._clientResetTimer) {
++ clearTimeout(this._clientResetTimer);
++ this._clientResetTimer = null;
++ console.warn(`[mocha-remote-ws] reconnect_recovered preserving_runner`);
++ }
+ if (this.client) {
+ this.debug("A client was already connected");
+ this.client.close(1013 /* try again later */, "Got a connection from another client");
+@@ -97,14 +104,20 @@
+ // Hang onto the client
+ this.client = ws;
+ this.client.on("message", this.handleMessage.bind(this, this.client));
+- this.client.once("close", this.handleReset);
++ this.client.once("close", (code) => this.handleClientDisconnect(code));
+ // If we already have a runner, it can run now that we have a client
+ if (this.runner) {
+- if (this.clientOptions) {
+- this.send({ action: "run", options: this.clientOptions });
++ if (this._awaitingInitialClientRun) {
++ if (this.clientOptions) {
++ this.send({ action: "run", options: this.clientOptions });
++ this._awaitingInitialClientRun = false;
++ }
++ else {
++ throw new Error("Internal error: Expected a clientOptions");
++ }
+ }
+ else {
+- throw new Error("Internal error: Expected a clientOptions");
++ this.debug("Client reconnected while runner active; resuming without re-run");
+ }
+ }
+ else if (this.config.autoRun) {
+@@ -130,6 +143,17 @@
throw new Error("Received a message from the client, but server wasn't running");
}
}
@@ -20,3 +66,58 @@ index ad9debe2086ab9b96e97a69aec966da8114ad102..e3c1c964956023df876dfcac3f73000b
else if (msg.action === "error") {
if (typeof msg.message !== "string") {
throw new Error("Expected 'error' action to have an error argument with a message");
+@@ -149,13 +173,46 @@
+ }
+ else {
+ throw err;
++ }
++ }
++ };
++ this.handleClientDisconnect = (code) => {
++ const transientCodes = [1006, 1001];
++ const graceMs = this.config.reconnectGraceMs ?? 15000;
++ if (this.runner && transientCodes.includes(code)) {
++ if (this._clientResetTimer) {
++ clearTimeout(this._clientResetTimer);
+ }
++ console.warn(`[mocha-remote-ws] transient_disconnect code=${code} grace_ms=${graceMs} preserving_runner`);
++ const client = this.client;
++ delete this.client;
++ if (client) {
++ client.removeAllListeners();
++ }
++ this._clientResetTimer = setTimeout(() => {
++ this._clientResetTimer = null;
++ if (!this.client) {
++ console.error(`[mocha-remote-ws] fatal_disconnect code=${code} grace_expired_ms=${graceMs}`);
++ this.handleReset();
++ }
++ }, graceMs);
++ return;
+ }
++ if (this._clientResetTimer) {
++ clearTimeout(this._clientResetTimer);
++ this._clientResetTimer = null;
++ }
++ this.handleReset();
+ };
+ /**
+ * Resets the server for another test run.
+ */
+ this.handleReset = () => {
++ if (this._clientResetTimer) {
++ clearTimeout(this._clientResetTimer);
++ this._clientResetTimer = null;
++ }
++ this._awaitingInitialClientRun = false;
+ // Forget everything about the runner and the client
+ const { runner, client } = this;
+ delete this.runner;
+@@ -263,6 +320,7 @@
+ // this.runner = new Mocha.Runner(this.suite, this.options.delay || false);
+ // TODO: Stub this to match the Runner's interface even better
+ this.runner = new FakeRunner_1.FakeRunner();
++ this._awaitingInitialClientRun = true;
+ // Attach event listeners to update stats
+ (0, stats_collector_1.createStatsCollector)(this.runner);
+ // Set the client options, to be passed to the next running client
diff --git a/eslint.config.mjs b/eslint.config.mjs
index aeeef28e43..8be8c6aeaa 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -62,7 +62,7 @@ export default defineConfig([
},
{
name: 'Prettier',
- ...eslintPluginPrettierRecommended.recommended,
+ ...eslintPluginPrettierRecommended,
},
{
name: 'React',
diff --git a/packages/ai/lib/requests/request.ts b/packages/ai/lib/requests/request.ts
index 84caf395c4..0d9d9bb596 100644
--- a/packages/ai/lib/requests/request.ts
+++ b/packages/ai/lib/requests/request.ts
@@ -125,7 +125,7 @@ function createAbortError(reason?: unknown): Error {
}
function getAbortSignalReason(signal?: AbortSignal): unknown {
- return (signal as AbortSignal & { reason?: unknown } | undefined)?.reason;
+ return (signal as (AbortSignal & { reason?: unknown }) | undefined)?.reason;
}
export class TemplateRequestUrl {
diff --git a/packages/analytics/e2e/analytics.e2e.js b/packages/analytics/e2e/analytics.e2e.js
index 13eda3d913..1886aa9ba6 100644
--- a/packages/analytics/e2e/analytics.e2e.js
+++ b/packages/analytics/e2e/analytics.e2e.js
@@ -15,12 +15,67 @@
*
*/
describe('analytics()', function () {
- beforeEach(async function () {
- const { getAnalytics, logEvent } = analyticsModular;
- await logEvent(getAnalytics(), 'screen_view');
+ // Probe session ID before any other analytics API usage (including suite beforeEach).
+ // Namespace APIs not tested here as only one set can run reliably, and modular-only is the future state.
+ describe('getSessionId() [modular — runs before all other analytics tests]', function () {
+ it('calls native fn without error', async function () {
+ const { getAnalytics, getSessionId } = analyticsModular;
+ await getSessionId(getAnalytics());
+ });
+
+ it('returns a non empty session ID', async function () {
+ if (Platform.other) {
+ this.skip();
+ }
+ const { getAnalytics, getSessionId } = analyticsModular;
+ let sessionId = await getSessionId(getAnalytics());
+ // On iOS it can take ~ 3 minutes for the session ID to be generated
+ // Otherwise, `Analytics uninitialized` error will be thrown
+ // On CI we're only going to give it a few tries
+ const retries = global.isCI ? 3 : 240;
+ let attemptsLeft = retries;
+ while (!sessionId && attemptsLeft > 0) {
+ await Utils.sleep(1000);
+ sessionId = await getSessionId(getAnalytics());
+ attemptsLeft -= 1;
+ }
+
+ // on CI this will be ignored so it doesn't flake
+ if (!sessionId && global.isCI) {
+ this.skip();
+ return;
+ }
+ if (!sessionId) {
+ return Promise.reject(
+ new Error('Firebase SDK did not return a session ID after 4 minutes'),
+ );
+ }
+
+ sessionId.should.not.equal(0);
+ });
+
+ it('returns a null value if session expires', async function () {
+ if (Platform.ios) {
+ // TODO - 20251030 iOS no longer correctly expires sessions
+ this.skip();
+ }
+ const { getAnalytics, getSessionId, setSessionTimeoutDuration } = analyticsModular;
+
+ // Set session duration to 1 millisecond
+ setSessionTimeoutDuration(getAnalytics(), 1);
+ // Wait 100 millisecond to ensure session expires
+ await Utils.sleep(100);
+ const sessionId = await getSessionId(getAnalytics());
+ should.equal(sessionId, null);
+ });
});
describe('firebase v8 compatibility', function () {
+ beforeEach(async function () {
+ const { getAnalytics, logEvent } = analyticsModular;
+ await logEvent(getAnalytics(), 'screen_view');
+ });
+
beforeEach(async function beforeEachTest() {
// @ts-ignore
globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true;
@@ -62,55 +117,6 @@ describe('analytics()', function () {
});
});
- describe('getSessionId()', function () {
- it('calls native fn without error', async function () {
- await firebase.analytics().getSessionId();
- });
-
- it('returns a non empty session ID', async function () {
- if (Platform.other) {
- this.skip();
- }
- let sessionId = await firebase.analytics().getSessionId();
- // On iOS it can take ~ 3 minutes for the session ID to be generated
- // Otherwise, `Analytics uninitialized` error will be thrown
- // On CI we're only going to give it a few tries
- const retries = global.isCI ? 3 : 240;
- let attemptsLeft = retries;
- while (!sessionId && attemptsLeft > 0) {
- await Utils.sleep(1000);
- sessionId = await firebase.analytics().getSessionId();
- attemptsLeft -= 1;
- }
-
- // on CI this will be ignored so it doesn't flake
- if (!sessionId && global.isCI) {
- this.skip();
- return;
- }
- if (!sessionId) {
- return Promise.reject(
- new Error('Firebase SDK did not return a session ID after 4 minutes'),
- );
- }
-
- sessionId.should.not.equal(0);
- });
-
- it('returns a null value if session expires', async function () {
- if (Platform.ios) {
- // TODO - 20251030 iOS no longer correctly expires sessions
- this.skip();
- }
- // Set session duration to 1 millisecond
- firebase.analytics().setSessionTimeoutDuration(1);
- // Wait 100 millisecond to ensure session expires
- await Utils.sleep(100);
- const sessionId = await firebase.analytics().getSessionId();
- should.equal(sessionId, null);
- });
- });
-
describe('setUserId()', function () {
it('allows a null values to be set', async function () {
await firebase.analytics().setUserId(null);
@@ -569,6 +575,11 @@ describe('analytics()', function () {
});
describe('modular', function () {
+ beforeEach(async function () {
+ const { getAnalytics, logEvent } = analyticsModular;
+ await logEvent(getAnalytics(), 'screen_view');
+ });
+
describe('getAnalytics', function () {
it('pass app as argument', function () {
const { getApp } = modular;
@@ -997,59 +1008,6 @@ describe('analytics()', function () {
});
});
- describe('getSessionId()', function () {
- it('calls native fn without error', async function () {
- const { getAnalytics, getSessionId } = analyticsModular;
- await getSessionId(getAnalytics());
- });
-
- it('returns a non empty session ID', async function () {
- if (Platform.other) {
- this.skip();
- }
- const { getAnalytics, getSessionId } = analyticsModular;
- let sessionId = await getSessionId(getAnalytics());
- // On iOS it can take ~ 3 minutes for the session ID to be generated
- // Otherwise, `Analytics uninitialized` error will be thrown
- // On CI we're only going to give it a few tries
- const retries = global.isCI ? 3 : 240;
- let attemptsLeft = retries;
- while (!sessionId && attemptsLeft > 0) {
- await Utils.sleep(1000);
- sessionId = await getSessionId(getAnalytics());
- attemptsLeft -= 1;
- }
-
- // on CI this will be ignored so it doesn't flake
- if (!sessionId && global.isCI) {
- this.skip();
- return;
- }
- if (!sessionId) {
- return Promise.reject(
- new Error('Firebase SDK did not return a session ID after 4 minutes'),
- );
- }
-
- sessionId.should.not.equal(0);
- });
-
- it('returns a null value if session expires', async function () {
- if (Platform.ios) {
- // TODO - 20251030 iOS no longer correctly expires sessions
- this.skip();
- }
- const { getAnalytics, getSessionId, setSessionTimeoutDuration } = analyticsModular;
-
- // Set session duration to 1 millisecond
- setSessionTimeoutDuration(getAnalytics(), 1);
- // Wait 100 millisecond to ensure session expires
- await Utils.sleep(100);
- const sessionId = await getSessionId(getAnalytics());
- should.equal(sessionId, null);
- });
- });
-
describe('setUserId()', function () {
it('allows a null values to be set', async function () {
const { getAnalytics, setUserId } = analyticsModular;
diff --git a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m
index 2d20d58433..7ecfb5bbb6 100644
--- a/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m
+++ b/packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m
@@ -165,7 +165,7 @@ - (dispatch_queue_t)methodQueue {
return;
}
completed = YES;
- DLog(@"Error getting session ID: timed out after 60 seconds");
+ DLog(@"getSessionId timed_out: no SDK callback within 60 seconds");
resolve([NSNull null]);
});
@@ -178,16 +178,18 @@ - (dispatch_queue_t)methodQueue {
// Occasionally sessionID is 0 despite nil error, reject as if it were an error
// https://github.com/firebase/firebase-ios-sdk/issues/15258
if (!error && [NSNumber numberWithLongLong:sessionID] == 0) {
- DLog(@"Error getting session ID: sessionID is zero despite nil error");
+ DLog(@"getSessionId zero_without_error: sessionID=0 (firebase-ios-sdk#15258)");
return resolve([NSNull null]);
}
if (error) {
- DLog(@"Error getting session ID: %@", error);
+ DLog(@"getSessionId sdk_error: domain=%@ code=%ld description=%@", error.domain,
+ (long)error.code, error.localizedDescription ?: @"(none)");
return resolve([NSNull null]);
- } else {
- return resolve([NSNumber numberWithLongLong:sessionID]);
}
+
+ DLog(@"getSessionId success: sessionID=%lld", sessionID);
+ return resolve([NSNumber numberWithLongLong:sessionID]);
}];
}
diff --git a/packages/analytics/plugin/src/ios/index.ts b/packages/analytics/plugin/src/ios/index.ts
index e642d277ac..cdf924da04 100644
--- a/packages/analytics/plugin/src/ios/index.ts
+++ b/packages/analytics/plugin/src/ios/index.ts
@@ -1,3 +1,6 @@
-import { withIosWithoutAdIdSupport, withIosGoogleAppMeasurementOnDeviceConversion } from './podfile';
+import {
+ withIosWithoutAdIdSupport,
+ withIosGoogleAppMeasurementOnDeviceConversion,
+} from './podfile';
export { withIosWithoutAdIdSupport, withIosGoogleAppMeasurementOnDeviceConversion };
diff --git a/packages/auth/__tests__/auth.test.ts b/packages/auth/__tests__/auth.test.ts
index f6672c1204..14a3c067e7 100644
--- a/packages/auth/__tests__/auth.test.ts
+++ b/packages/auth/__tests__/auth.test.ts
@@ -1340,7 +1340,9 @@ describe('Auth', function () {
_setUserCredential: setUserCredential,
};
const user = new User(authInternal as any, { uid: 'test-uid' } as any);
- await user.reauthenticateWithRedirect({ toObject: () => ({ providerId: 'google.com' }) } as any);
+ await user.reauthenticateWithRedirect({
+ toObject: () => ({ providerId: 'google.com' }),
+ } as any);
expect(setUserCredential).toHaveBeenCalled();
});
});
diff --git a/packages/auth/lib/ActionCodeURL.ts b/packages/auth/lib/ActionCodeURL.ts
index 7416b16b92..639333377b 100644
--- a/packages/auth/lib/ActionCodeURL.ts
+++ b/packages/auth/lib/ActionCodeURL.ts
@@ -42,12 +42,8 @@ function querystringDecode(query: string): Record {
}
const separatorIndex = part.indexOf('=');
- const key = decodeURIComponent(
- separatorIndex >= 0 ? part.slice(0, separatorIndex) : part,
- );
- const value = decodeURIComponent(
- separatorIndex >= 0 ? part.slice(separatorIndex + 1) : '',
- );
+ const key = decodeURIComponent(separatorIndex >= 0 ? part.slice(0, separatorIndex) : part);
+ const value = decodeURIComponent(separatorIndex >= 0 ? part.slice(separatorIndex + 1) : '');
decoded[key] = value;
}
@@ -81,9 +77,7 @@ function parseMode(mode: string | null): string | null {
*/
function parseDeepLink(url: string): string {
const link = querystringDecode(extractQuerystring(url)).link;
- const doubleDeepLink = link
- ? querystringDecode(extractQuerystring(link)).deep_link_id
- : null;
+ const doubleDeepLink = link ? querystringDecode(extractQuerystring(link)).deep_link_id : null;
const iOSDeepLink = querystringDecode(extractQuerystring(url)).deep_link_id;
const iOSDoubleDeepLink = iOSDeepLink
? querystringDecode(extractQuerystring(iOSDeepLink)).link
diff --git a/packages/auth/lib/User.ts b/packages/auth/lib/User.ts
index 14b2e1bd91..735a935f7c 100644
--- a/packages/auth/lib/User.ts
+++ b/packages/auth/lib/User.ts
@@ -241,8 +241,8 @@ export default class User {
}
return this._auth.native.sendEmailVerification(actionCodeSettings).then(user => {
- this._auth._setUser(user);
- });
+ this._auth._setUser(user);
+ });
}
toJSON(): object {
@@ -250,15 +250,13 @@ export default class User {
}
unlink(providerId: string): Promise {
- return this._auth.native
- .unlink(providerId)
- .then(user => {
- const updatedUser = this._auth._setUser(user);
- if (!updatedUser) {
- throw new Error('firebase.auth.User.unlink() returned no user after unlinking provider.');
- }
- return updatedUser;
- });
+ return this._auth.native.unlink(providerId).then(user => {
+ const updatedUser = this._auth._setUser(user);
+ if (!updatedUser) {
+ throw new Error('firebase.auth.User.unlink() returned no user after unlinking provider.');
+ }
+ return updatedUser;
+ });
}
updateEmail(email: string): Promise {
@@ -361,8 +359,8 @@ export default class User {
}
return this._auth.native.verifyBeforeUpdateEmail(newEmail, actionCodeSettings).then(user => {
- this._auth._setUser(user);
- });
+ this._auth._setUser(user);
+ });
}
/**
diff --git a/packages/auth/lib/credentials/EmailAuthCredential.ts b/packages/auth/lib/credentials/EmailAuthCredential.ts
index deed376f5d..94c3f8366c 100644
--- a/packages/auth/lib/credentials/EmailAuthCredential.ts
+++ b/packages/auth/lib/credentials/EmailAuthCredential.ts
@@ -25,11 +25,7 @@ type EmailCredentialJSON = {
};
export class EmailAuthCredential extends AuthCredential {
- constructor(
- signInMethod: 'password' | 'emailLink',
- email: string,
- password: string,
- ) {
+ constructor(signInMethod: 'password' | 'emailLink', email: string, password: string) {
super(signInMethod, signInMethod, email, password);
}
diff --git a/packages/auth/lib/credentials/OAuthCredential.ts b/packages/auth/lib/credentials/OAuthCredential.ts
index d346f2d62a..856a008b96 100644
--- a/packages/auth/lib/credentials/OAuthCredential.ts
+++ b/packages/auth/lib/credentials/OAuthCredential.ts
@@ -38,7 +38,10 @@ type OAuthCredentialParams = {
bridgeSecret?: string;
};
-function resolveOAuthBridgeFields(params: OAuthCredentialParams): { token: string; secret: string } {
+function resolveOAuthBridgeFields(params: OAuthCredentialParams): {
+ token: string;
+ secret: string;
+} {
if (params.bridgeToken !== undefined || params.bridgeSecret !== undefined) {
return {
token: params.bridgeToken ?? '',
diff --git a/packages/auth/lib/getMultiFactorResolver.ts b/packages/auth/lib/getMultiFactorResolver.ts
index c5b4205264..eecf39e183 100644
--- a/packages/auth/lib/getMultiFactorResolver.ts
+++ b/packages/auth/lib/getMultiFactorResolver.ts
@@ -23,7 +23,9 @@ export function getMultiFactorResolver(
error: ErrorWithResolver,
): FirebaseAuthTypes.MultiFactorResolver | null {
if (isOther) {
- return auth.native.getMultiFactorResolver(error) as FirebaseAuthTypes.MultiFactorResolver | null;
+ return auth.native.getMultiFactorResolver(
+ error,
+ ) as FirebaseAuthTypes.MultiFactorResolver | null;
}
if (
error.hasOwnProperty('userInfo') &&
diff --git a/packages/auth/lib/modular.ts b/packages/auth/lib/modular.ts
index 9a4ed40a9b..8ec3628184 100644
--- a/packages/auth/lib/modular.ts
+++ b/packages/auth/lib/modular.ts
@@ -97,17 +97,9 @@ import type {
type AnyFn = (...args: any[]) => any;
type UserModuleInternal = UserInternal;
-type MultiFactorInfoInternal =
- | MultiFactorInfo
- | MultiFactorResolverResultInternal['hints'][number];
+type MultiFactorInfoInternal = MultiFactorInfo | MultiFactorResolverResultInternal['hints'][number];
-export {
- ActionCodeOperation,
- FactorId,
- OperationType,
- ProviderId,
- SignInMethod,
-};
+export { ActionCodeOperation, FactorId, OperationType, ProviderId, SignInMethod };
function appWithAuth(app?: FirebaseApp): AppWithAuthInternal {
return (app ? getApp(app.name) : getApp()) as unknown as AppWithAuthInternal;
@@ -128,9 +120,7 @@ type AdditionalUserInfoSource = {
username?: string | null;
} & Record;
-function normalizeAdditionalUserInfo(
- info: AdditionalUserInfoSource,
-): AdditionalUserInfoNative {
+function normalizeAdditionalUserInfo(info: AdditionalUserInfoSource): AdditionalUserInfoNative {
return {
...info,
isNewUser: Boolean(info.isNewUser),
@@ -245,9 +235,7 @@ function normalizeMultiFactorUser(multiFactorUser: MultiFactorUserResultInternal
getSession: () => multiFactorUser.getSession(),
enroll: (assertion, displayName) => multiFactorUser.enroll(assertion, displayName),
unenroll: option =>
- multiFactorUser.unenroll(
- option as Parameters[0],
- ),
+ multiFactorUser.unenroll(option as Parameters[0]),
};
}
@@ -417,11 +405,15 @@ export function createUserWithEmailAndPassword(
password: string,
): Promise {
const authInternal = getAuthInternal(auth);
- return callAuthMethod(authInternal, authInternal.createUserWithEmailAndPassword, email, password).then(
- userCredential =>
- normalizeUserCredential(userCredential, {
- operationType: OperationType.SIGN_IN,
- }),
+ return callAuthMethod(
+ authInternal,
+ authInternal.createUserWithEmailAndPassword,
+ email,
+ password,
+ ).then(userCredential =>
+ normalizeUserCredential(userCredential, {
+ operationType: OperationType.SIGN_IN,
+ }),
);
}
@@ -636,11 +628,15 @@ export function signInWithEmailAndPassword(
password: string,
): Promise {
const authInternal = getAuthInternal(auth);
- return callAuthMethod(authInternal, authInternal.signInWithEmailAndPassword, email, password).then(
- userCredential =>
- normalizeUserCredential(userCredential, {
- operationType: OperationType.SIGN_IN,
- }),
+ return callAuthMethod(
+ authInternal,
+ authInternal.signInWithEmailAndPassword,
+ email,
+ password,
+ ).then(userCredential =>
+ normalizeUserCredential(userCredential, {
+ operationType: OperationType.SIGN_IN,
+ }),
);
}
@@ -1108,9 +1104,7 @@ export function verifyBeforeUpdateEmail(
*
* @remarks Returns firebase-js-sdk core fields plus any extra native keys copied from the bridge.
*/
-export function getAdditionalUserInfo(
- userCredential: UserCredential,
-): AdditionalUserInfo | null {
+export function getAdditionalUserInfo(userCredential: UserCredential): AdditionalUserInfo | null {
if (userCredential.additionalUserInfo) {
return userCredential.additionalUserInfo;
}
diff --git a/packages/auth/lib/multiFactor.ts b/packages/auth/lib/multiFactor.ts
index d95ffc7536..846ba65ded 100644
--- a/packages/auth/lib/multiFactor.ts
+++ b/packages/auth/lib/multiFactor.ts
@@ -1,8 +1,5 @@
import { reload } from './modular';
-import type {
- MultiFactorAssertion as ModularMultiFactorAssertion,
- User,
-} from './types/auth';
+import type { MultiFactorAssertion as ModularMultiFactorAssertion, User } from './types/auth';
import type { FirebaseAuthTypes } from './types/namespaced';
import type { AuthInternal, MultiFactorEnrollmentAssertionInternal } from './types/internal';
diff --git a/packages/auth/lib/namespaced.ts b/packages/auth/lib/namespaced.ts
index 6d11c08929..72cfb78195 100644
--- a/packages/auth/lib/namespaced.ts
+++ b/packages/auth/lib/namespaced.ts
@@ -623,7 +623,10 @@ class FirebaseAuthModule extends FirebaseModule {
email: string,
actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings,
): Promise {
- return this.native.sendSignInLinkToEmail(email, this._resolveActionCodeSettings(actionCodeSettings));
+ return this.native.sendSignInLinkToEmail(
+ email,
+ this._resolveActionCodeSettings(actionCodeSettings),
+ );
}
isSignInWithEmailLink(emailLink: string): Promise {
diff --git a/packages/auth/lib/providers/OIDCAuthProvider.ts b/packages/auth/lib/providers/OIDCAuthProvider.ts
index 51cdd13ce0..5fd3be3f35 100644
--- a/packages/auth/lib/providers/OIDCAuthProvider.ts
+++ b/packages/auth/lib/providers/OIDCAuthProvider.ts
@@ -38,11 +38,7 @@ export default class OIDCAuthProvider {
return providerId;
}
- static credential(
- oidcSuffix: string,
- idToken: string,
- accessToken?: string,
- ): AuthCredentialType {
+ static credential(oidcSuffix: string, idToken: string, accessToken?: string): AuthCredentialType {
const resolvedProviderId = providerId + oidcSuffix;
return new AuthCredential(resolvedProviderId, resolvedProviderId, idToken, accessToken ?? '');
}
diff --git a/packages/auth/lib/types/internal.ts b/packages/auth/lib/types/internal.ts
index 7f3231b237..ee2c2fdffb 100644
--- a/packages/auth/lib/types/internal.ts
+++ b/packages/auth/lib/types/internal.ts
@@ -329,9 +329,10 @@ export interface RNFBAuthModule {
token: string,
secret?: string | null,
): Promise;
- updateProfile(
- updates: { displayName?: string | null; photoURL?: string | null },
- ): Promise;
+ updateProfile(updates: {
+ displayName?: string | null;
+ photoURL?: string | null;
+ }): Promise;
verifyBeforeUpdateEmail(
newEmail: string,
actionCodeSettings?: FirebaseAuthTypes.ActionCodeSettings,
@@ -381,13 +382,12 @@ export type AuthInternal = Auth & {
email: string,
actionCodeSettings?: ActionCodeSettings | null,
): Promise;
- sendSignInLinkToEmail(
- email: string,
- actionCodeSettings?: ActionCodeSettings,
- ): Promise;
+ sendSignInLinkToEmail(email: string, actionCodeSettings?: ActionCodeSettings): Promise;
setLanguageCode(code: string | null): Promise;
signInAnonymously(): Promise;
- signInWithCredential(credential: AuthCredential): Promise;
+ signInWithCredential(
+ credential: AuthCredential,
+ ): Promise;
signInWithCustomToken(customToken: string): Promise;
signInWithEmailAndPassword(
email: string,
@@ -443,8 +443,12 @@ export type UserInternal = FirebaseAuthTypes.User & {
_auth?: AuthInternal;
_user?: NativeUserInternal;
getIdTokenResult(forceRefresh?: boolean): Promise;
- linkWithCredential(credential: AuthCredential): Promise;
- linkWithPopup(provider: AuthProviderWithObjectInternal): Promise;
+ linkWithCredential(
+ credential: AuthCredential,
+ ): Promise;
+ linkWithPopup(
+ provider: AuthProviderWithObjectInternal,
+ ): Promise;
linkWithRedirect(
provider: AuthProviderWithObjectInternal,
): Promise;
diff --git a/packages/database/e2e/query/onChildAdded.e2e.js b/packages/database/e2e/query/onChildAdded.e2e.js
index c70201ae00..b7a5620d21 100644
--- a/packages/database/e2e/query/onChildAdded.e2e.js
+++ b/packages/database/e2e/query/onChildAdded.e2e.js
@@ -15,12 +15,7 @@
*
*/
-const {
- PATH,
- seed,
- wipe,
- waitForNativeDbListenerRegistration,
-} = require('../helpers');
+const { PATH, seed, wipe, waitForNativeDbListenerRegistration } = require('../helpers');
const TEST_PATH = `${PATH}/on`;
diff --git a/packages/database/e2e/query/onChildChanged.e2e.js b/packages/database/e2e/query/onChildChanged.e2e.js
index 2f10880455..a89ea29e1c 100644
--- a/packages/database/e2e/query/onChildChanged.e2e.js
+++ b/packages/database/e2e/query/onChildChanged.e2e.js
@@ -15,12 +15,7 @@
*
*/
-const {
- PATH,
- seed,
- wipe,
- waitForNativeDbListenerReady,
-} = require('../helpers');
+const { PATH, seed, wipe, waitForNativeDbListenerReady } = require('../helpers');
const TEST_PATH = `${PATH}/on`;
diff --git a/packages/database/e2e/query/onChildMoved.e2e.js b/packages/database/e2e/query/onChildMoved.e2e.js
index 5247dcacfb..0de96c552f 100644
--- a/packages/database/e2e/query/onChildMoved.e2e.js
+++ b/packages/database/e2e/query/onChildMoved.e2e.js
@@ -15,12 +15,7 @@
*
*/
-const {
- PATH,
- seed,
- wipe,
- waitForNativeDbListenerRegistration,
-} = require('../helpers');
+const { PATH, seed, wipe, waitForNativeDbListenerRegistration } = require('../helpers');
const TEST_PATH = `${PATH}/on`;
diff --git a/packages/database/e2e/query/onChildRemoved.e2e.js b/packages/database/e2e/query/onChildRemoved.e2e.js
index 709487dc42..fa29f2838f 100644
--- a/packages/database/e2e/query/onChildRemoved.e2e.js
+++ b/packages/database/e2e/query/onChildRemoved.e2e.js
@@ -15,12 +15,7 @@
*
*/
-const {
- PATH,
- seed,
- wipe,
- waitForNativeDbListenerReady,
-} = require('../helpers');
+const { PATH, seed, wipe, waitForNativeDbListenerReady } = require('../helpers');
const TEST_PATH = `${PATH}/on`;
diff --git a/packages/firestore/consumer-type-test.ts b/packages/firestore/consumer-type-test.ts
index f8bbcbb209..0155d604fb 100644
--- a/packages/firestore/consumer-type-test.ts
+++ b/packages/firestore/consumer-type-test.ts
@@ -314,9 +314,7 @@ const nsDocRef = nsColl.doc('alice');
const nsQuery = nsColl.where('name', '==', 'test');
nsDocRef.set({ name: 'Alice', count: 1 }).then(() => {});
-nsDocRef
- .set({ name: 'Alice' }, { merge: true })
- .then(() => {});
+nsDocRef.set({ name: 'Alice' }, { merge: true }).then(() => {});
nsDocRef.update({ count: 2 }).then(() => {});
nsDocRef.update('count', 3).then(() => {});
@@ -401,13 +399,15 @@ void nsLoadTask.then(() => {});
const nsNamed = nsFirestore.namedQuery('my-query');
void nsNamed;
-nsFirestore.runTransaction(async (tx: FirebaseFirestoreTypes.Transaction) => {
- const snap = await tx.get(nsDocRef);
- if (snap.exists()) {
- tx.update(nsDocRef, { count: ((snap.data() as { count?: number })?.count ?? 0) + 1 });
- }
- return null;
-}).then(() => {});
+nsFirestore
+ .runTransaction(async (tx: FirebaseFirestoreTypes.Transaction) => {
+ const snap = await tx.get(nsDocRef);
+ if (snap.exists()) {
+ tx.update(nsDocRef, { count: ((snap.data() as { count?: number })?.count ?? 0) + 1 });
+ }
+ return null;
+ })
+ .then(() => {});
// ----- Firestore instance: persistence and network -----
nsFirestore.clearPersistence().then(() => {});
@@ -452,13 +452,15 @@ const nsArrayRemove = firebase.firestore.FieldValue.arrayRemove(1);
void nsArrayRemove;
const nsIncrement = firebase.firestore.FieldValue.increment(1);
-nsDocRef.set({
- name: 'x',
- deleted: nsDelete,
- ts: nsServerTs,
- arr: nsArrayUnion,
- cnt: nsIncrement,
-}).then(() => {});
+nsDocRef
+ .set({
+ name: 'x',
+ deleted: nsDelete,
+ ts: nsServerTs,
+ arr: nsArrayUnion,
+ cnt: nsIncrement,
+ })
+ .then(() => {});
// ----- withConverter (namespaced) -----
interface User {
@@ -481,7 +483,6 @@ nsDocWithConv.get().then((snap: FirebaseFirestoreTypes.DocumentSnapshot) =
if (u) void [u.name, u.age];
});
-
// ----- getFirestore -----
const modFirestore1 = getFirestore();
void modFirestore1.app.name;
@@ -970,10 +971,7 @@ const pipelineUnion = pipelineDb
.collection('cities/sf/restaurants')
.where(field('type').equal('Chinese'))
.union(
- pipelineDb
- .pipeline()
- .collection('cities/ny/restaurants')
- .where(field('type').equal('Italian')),
+ pipelineDb.pipeline().collection('cities/ny/restaurants').where(field('type').equal('Italian')),
)
.where(field('rating').greaterThanOrEqual(4.5))
.sort(field('__name__').descending());
@@ -984,10 +982,7 @@ const pipelineWithTransforms = pipelineDb
.collection('books')
.where(
pipelineOr(
- pipelineAnd(
- field('rating').greaterThan(4),
- lessThan(field('price'), constant(10)),
- ),
+ pipelineAnd(field('rating').greaterThan(4), lessThan(field('price'), constant(10))),
field('genre').equal('Fantasy'),
),
)
@@ -996,9 +991,7 @@ const pipelineWithTransforms = pipelineDb
.select(
field('fullTitle'),
field('rating').greaterThan(4).as('isTopRated'),
- arrayContainsAny(field('genre'), ['Fantasy', constant('Sci-Fi')]).as(
- 'matchesGenre',
- ),
+ arrayContainsAny(field('genre'), ['Fantasy', constant('Sci-Fi')]).as('matchesGenre'),
)
.sort(Ordering.of(field('rating')).descending(), field('__name__').ascending())
.offset(1)
@@ -1015,22 +1008,22 @@ const pipelineAggregateDistinct = pipelineDb
pipelineAverage('population').as('populationAvg'),
maximum('population').as('populationMax'),
],
- groups: [
- field('country').as('country'),
- toLower(field('state')).as('normalizedState'),
- ],
+ groups: [field('country').as('country'), toLower(field('state')).as('normalizedState')],
})
.where(field('populationTotal').greaterThan(1000))
.distinct(field('normalizedState'), 'country');
void pipelineAggregateDistinct;
-const pipelineFindNearest = pipelineDb.pipeline().collection('cities').findNearest({
- field: 'embedding',
- vectorValue: [1.5, 2.345],
- distanceMeasure: 'COSINE',
- distanceField: 'computedDistance',
- limit: 10,
-});
+const pipelineFindNearest = pipelineDb
+ .pipeline()
+ .collection('cities')
+ .findNearest({
+ field: 'embedding',
+ vectorValue: [1.5, 2.345],
+ distanceMeasure: 'COSINE',
+ distanceField: 'computedDistance',
+ limit: 10,
+ });
void pipelineFindNearest;
const pipelineSampleAndUnnest = pipelineDb
@@ -1113,7 +1106,11 @@ const _cStr: Expression = constant('hello');
const _cBool: BooleanExpression = constant(true);
const _cNull: Expression = constant(null);
const _cUnknown: Expression = constant({ nested: true });
-void _cNum; void _cStr; void _cBool; void _cNull; void _cUnknown;
+void _cNum;
+void _cStr;
+void _cBool;
+void _cNull;
+void _cUnknown;
// ----- Comparison: standalone overloads -----
// greaterThan(Expression, Expression) | greaterThan(Expression, value)
@@ -1536,11 +1533,9 @@ const pipelineComparisonOps = xDb
)
.select(
field('sku'),
- conditional(
- field('stock').greaterThan(0),
- constant('in-stock'),
- constant('out-of-stock'),
- ).as('availability'),
+ conditional(field('stock').greaterThan(0), constant('in-stock'), constant('out-of-stock')).as(
+ 'availability',
+ ),
isType(field('value'), 'string').as('isString'),
logicalMaximum(field('bidA'), field('bidB')).as('topBid'),
logicalMinimum(field('askA'), field('askB')).as('bottomAsk'),
@@ -1595,10 +1590,7 @@ const pipelineStringOps = xDb
stringContains(field('bio'), 'developer'),
like('role', 'eng%'),
regexContains(field('phone'), '^\\+1'),
- xor(
- field('isPublic').equal(true),
- field('isVerified').equal(true),
- ),
+ xor(field('isPublic').equal(true), field('isVerified').equal(true)),
),
)
.addFields(
@@ -1722,10 +1714,7 @@ const pipelineAllAggregates = xDb
arrayAggDistinct(field('category')).as('distinctCategories'),
arrayAggDistinct('category').as('distinctCategories2'),
],
- groups: [
- field('country').as('country'),
- toLower(field('state')).as('normalizedState'),
- ],
+ groups: [field('country').as('country'), toLower(field('state')).as('normalizedState')],
});
void pipelineAllAggregates;
diff --git a/packages/firestore/e2e/helpers.js b/packages/firestore/e2e/helpers.js
index 800e633670..c44aa4e156 100644
--- a/packages/firestore/e2e/helpers.js
+++ b/packages/firestore/e2e/helpers.js
@@ -43,9 +43,7 @@ exports.wipe = async function wipe(debug = false, databaseId = '(default)', retr
if (!response.ok) {
const body = await response.text();
- throw new Error(
- `Firestore wipe failed: HTTP ${response.status} ${body.slice(0, 200)}`,
- );
+ throw new Error(`Firestore wipe failed: HTTP ${response.status} ${body.slice(0, 200)}`);
}
if (debug) {
diff --git a/packages/in-app-messaging/lib/modular.ts b/packages/in-app-messaging/lib/modular.ts
index f0174d67d8..2e2e63fefd 100644
--- a/packages/in-app-messaging/lib/modular.ts
+++ b/packages/in-app-messaging/lib/modular.ts
@@ -23,7 +23,9 @@ import {
import type { InAppMessaging } from './types/in-app-messaging';
import type { InAppMessagingWithDeprecationArg } from './types/internal';
-function withModularDeprecationArg(inAppMessaging: InAppMessaging): InAppMessagingWithDeprecationArg {
+function withModularDeprecationArg(
+ inAppMessaging: InAppMessaging,
+): InAppMessagingWithDeprecationArg {
return inAppMessaging as InAppMessagingWithDeprecationArg;
}
diff --git a/packages/installations/README.md b/packages/installations/README.md
index 06f49e5e91..cee8142fb9 100644
--- a/packages/installations/README.md
+++ b/packages/installations/README.md
@@ -19,7 +19,7 @@
-----
+---
Entry point for Firebase installations.
@@ -29,7 +29,6 @@ The Firebase installations service:
- provides an auth token for a Firebase installation
- provides a API to perform GDPR-compliant deletion of a Firebase installation.
-
[> Learn More](https://firebase.google.com/docs/projects/manage-installations)
## Installation
@@ -42,14 +41,14 @@ yarn add @react-native-firebase/installations
## Documentation
- - [Guides](https://rnfirebase.io/installations/usage/)
- - [Reference](https://rnfirebase.io/reference/installations)
+- [Guides](https://rnfirebase.io/installations/usage/)
+- [Reference](https://rnfirebase.io/reference/installations)
## License
- See [LICENSE](/LICENSE)
-----
+---
@@ -58,4 +57,4 @@ yarn add @react-native-firebase/installations
-----
+---
diff --git a/packages/messaging/README.md b/packages/messaging/README.md
index e7dc3e770f..c90d6551cd 100644
--- a/packages/messaging/README.md
+++ b/packages/messaging/README.md
@@ -19,7 +19,7 @@
-----
+---
React Native Firebase provides native integration of Firebase Cloud Messaging (FCM) for both Android & iOS. FCM is a
cost free service, allowing for server-device and device-device communication.
@@ -42,6 +42,7 @@ yarn add @react-native-firebase/messaging
- [Reference](https://rnfirebase.io/reference/messaging)
### Additional Topics
+
- [iOS Permissions](https://rnfirebase.io/messaging/ios-permissions)
- [Notifications](https://rnfirebase.io/messaging/notifications)
- [Server Integration](https://rnfirebase.io/messaging/server-integration)
@@ -50,7 +51,7 @@ yarn add @react-native-firebase/messaging
- See [LICENSE](/LICENSE)
-----
+---
@@ -59,4 +60,4 @@ yarn add @react-native-firebase/messaging
-----
+---
diff --git a/packages/phone-number-verification/lib/modular.ts b/packages/phone-number-verification/lib/modular.ts
index 1c5874afd7..9abb6f729c 100644
--- a/packages/phone-number-verification/lib/modular.ts
+++ b/packages/phone-number-verification/lib/modular.ts
@@ -77,9 +77,7 @@ export function enableTestSession(token: string): Promise {
* @returns Array of support results, one per SIM slot.
* @see https://firebase.google.com/docs/phone-number-verification
*/
-export function getVerificationSupportInfo(
- simSlot?: number,
-): Promise {
+export function getVerificationSupportInfo(simSlot?: number): Promise {
if (simSlot !== undefined) {
return getNativeModule().getVerificationSupportInfoForSimSlot(simSlot);
}
diff --git a/packages/vertexai/README.md b/packages/vertexai/README.md
index d179e79e14..b825eedfa8 100644
--- a/packages/vertexai/README.md
+++ b/packages/vertexai/README.md
@@ -15,12 +15,12 @@ To start using the new SDK, import the `@react-native-firebase/ai` package and u
```javascript
// BEFORE - using firebase/vertexai
-import { initializeApp } from "firebase/app";
-import { getVertexAI, getGenerativeModel } from "firebase/vertexai"; // Remove this
+import { initializeApp } from 'firebase/app';
+import { getVertexAI, getGenerativeModel } from 'firebase/vertexai'; // Remove this
// AFTER - using firebase/ai
-import { initializeApp } from "firebase/app";
-import { getAI, getGenerativeModel } from "firebase/ai"; // Add this
+import { initializeApp } from 'firebase/app';
+import { getAI, getGenerativeModel } from 'firebase/ai'; // Add this
```
---
diff --git a/scripts/repro-android-build-flake.sh b/scripts/repro-android-build-flake.sh
new file mode 100755
index 0000000000..f5a302de93
--- /dev/null
+++ b/scripts/repro-android-build-flake.sh
@@ -0,0 +1,123 @@
+#!/bin/bash
+# Reproduce intermittent Android packageDebugAndroidTest / IncrementalSplitterRunnable
+# failures under cold install + parallel host load (mirrors run-full-tests.sh phase 2–5).
+#
+# Usage:
+# ./scripts/repro-android-build-flake.sh [attempts]
+# ATTEMPTS=5 ./scripts/repro-android-build-flake.sh
+#
+# Logs:
+# /tmp/rnfb-android-flake-repro.log full output
+# /tmp/rnfb-android-flake-repro.summary pass/fail per attempt
+
+set -u
+
+REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$REPO_ROOT"
+
+ATTEMPTS="${1:-${ATTEMPTS:-5}}"
+LOG="${LOG:-/tmp/rnfb-android-flake-repro.log}"
+SUMMARY="${SUMMARY:-/tmp/rnfb-android-flake-repro.summary}"
+
+: >"$LOG"
+: >"$SUMMARY"
+
+log() {
+ echo "$1" | tee -a "$LOG" "$SUMMARY"
+}
+
+# Plain rm only — do not call yarn build:clean before node_modules exists (rimraf lives there).
+clean_workspace_artifacts() {
+ log " stopping gradle daemons..."
+ (cd tests/android && ./gradlew --stop >>"$LOG" 2>&1) || true
+
+ log " removing android/ios/macos build dirs..."
+ rm -rf \
+ tests/android/build \
+ tests/android/app/build \
+ tests/android/.gradle \
+ tests/android/app/.cxx \
+ tests/ios/build \
+ tests/macos/build \
+ tests/dist
+
+ log " removing all node_modules..."
+ find . -name node_modules -type d -prune -exec rm -rf {} + 2>/dev/null || true
+}
+
+cold_reset() {
+ log "=== COLD RESET $(date -Iseconds) ==="
+ clean_workspace_artifacts
+
+ log " yarn install (codegen)..."
+ yarn install >>"$LOG" 2>&1 || return 1
+
+ log " gradlew clean (post-codegen)..."
+ (cd tests/android && ./gradlew clean --no-build-cache >>"$LOG" 2>&1) || true
+
+ log " pod install (ios + macos)..."
+ (yarn tests:ios:pod:install >>"$LOG" 2>&1 & yarn tests:macos:pod:install >>"$LOG" 2>&1 & wait) || return 1
+}
+
+# Android build with maximum coldness; other jobs match run-full-tests.sh parallel block.
+run_android_build_cold() {
+ (
+ cd tests/android
+ ./gradlew-with-worker-cap.sh \
+ assembleDebug assembleAndroidTest lintDebug \
+ -DtestBuildType=debug \
+ --warning-mode all \
+ --stacktrace \
+ --no-build-cache \
+ --rerun-tasks
+ ) >>"$LOG" 2>&1
+}
+
+parallel_verify() {
+ log "=== PARALLEL VERIFY $(date -Iseconds) ==="
+ local pids=()
+ local failed=0
+
+ yarn tests:ios:build >>"$LOG" 2>&1 & pids+=($!)
+ yarn tests:macos:build >>"$LOG" 2>&1 & pids+=($!)
+ run_android_build_cold & pids+=($!)
+ yarn compare:types >>"$LOG" 2>&1 & pids+=($!)
+ yarn lint:js >>"$LOG" 2>&1 & pids+=($!)
+ yarn lint:ios:check >>"$LOG" 2>&1 & pids+=($!)
+ yarn lint:markdown >>"$LOG" 2>&1 & pids+=($!)
+ yarn lint:spellcheck >>"$LOG" 2>&1 & pids+=($!)
+ yarn tests:jest >>"$LOG" 2>&1 & pids+=($!)
+
+ for pid in "${pids[@]}"; do
+ wait "$pid" || failed=1
+ done
+
+ return "$failed"
+}
+
+extract_failure_hints() {
+ grep -nE \
+ 'IncrementalSplitter|packageDebugAndroidTest FAILED|Caused by|NoSuchFile|OutOfMemory|BUILD FAILED|Command failed' \
+ "$LOG" | tail -40 | tee -a "$SUMMARY" || true
+}
+
+pass_count=0
+fail_count=0
+
+for i in $(seq 1 "$ATTEMPTS"); do
+ log ""
+ log "=== ATTEMPT $i/$ATTEMPTS $(date -Iseconds) ==="
+
+ if cold_reset && parallel_verify; then
+ log "ATTEMPT $i: PASS $(date -Iseconds)"
+ pass_count=$((pass_count + 1))
+ else
+ log "ATTEMPT $i: FAIL $(date -Iseconds)"
+ fail_count=$((fail_count + 1))
+ extract_failure_hints
+ fi
+done
+
+log ""
+log "=== SUMMARY: $pass_count passed, $fail_count failed (of $ATTEMPTS) ==="
+log "Full log: $LOG"
diff --git a/scripts/run-full-tests.sh b/scripts/run-full-tests.sh
index bfc853e2b4..8c4be5359c 100755
--- a/scripts/run-full-tests.sh
+++ b/scripts/run-full-tests.sh
@@ -5,6 +5,7 @@
set -e
# Create temporary directory for logs
TMP_DIR=$(mktemp -d)
+echo "Step logs directory: $TMP_DIR"
# Clean up any stale metro bundler or firebase emulator processes
function terminate_testing_processes() {
@@ -37,8 +38,8 @@ run_yarn_script() {
# Run the yarn command and redirect output to log file
if ! yarn "$script_name" > "$log_file" 2>&1; then
echo "Command failed: yarn $script_name"
+ echo "Full log preserved at: $log_file"
cat "$log_file"
- rm -f "$log_file"
return 1
fi
@@ -50,7 +51,10 @@ run_yarn_script() {
run_yarn_scripts_parallel() {
local scripts=("$@")
local pids=()
- local pid failed=0
+ local script_names=()
+ local failed_scripts=()
+ local i=0
+ local failed=0
(
trap 'kill 0' SIGINT
@@ -58,12 +62,24 @@ run_yarn_scripts_parallel() {
for script_name in "${scripts[@]}"; do
run_yarn_script "$script_name" &
pids+=($!)
+ script_names+=("$script_name")
done
for pid in "${pids[@]}"; do
- wait "$pid" || failed=1
+ if ! wait "$pid"; then
+ failed=1
+ failed_scripts+=("${script_names[$i]}")
+ fi
+ i=$((i + 1))
done
+ if [ "$failed" -ne 0 ]; then
+ echo "Parallel step failed. Preserved logs in: $TMP_DIR"
+ for script_name in "${failed_scripts[@]}"; do
+ echo " - ${TMP_DIR}/${script_name}.log"
+ done
+ fi
+
exit "$failed"
) || return 1
}
@@ -72,13 +88,13 @@ echo "Starting full test execution..."
# 1. Dependency Installation
echo "Installing dependencies..."
-run_yarn_script "install" || { echo "yarn install failed"; exit 1; }
+run_yarn_script "install" || { echo "yarn install failed. Logs preserved in: $TMP_DIR"; exit 1; }
echo "Installing iOS and macOS pods in parallel..."
run_yarn_scripts_parallel \
"tests:ios:pod:install" \
"tests:macos:pod:install" \
- || { echo "Pod install failed"; exit 1; }
+ || { echo "Pod install failed. Logs preserved in: $TMP_DIR"; exit 1; }
# 2–5. Builds, typechecks, lint, and unit tests (all parallel)
echo "Running builds, typechecks, lint, and unit tests in parallel..."
@@ -92,7 +108,7 @@ run_yarn_scripts_parallel \
"lint:markdown" \
"lint:spellcheck" \
"tests:jest" \
- || { echo "Parallel verification failed"; exit 1; }
+ || { echo "Parallel verification failed. Logs preserved in: $TMP_DIR"; exit 1; }
# 6. E2E Tests with Flakiness Tolerance
echo "Running E2E tests..."
@@ -114,10 +130,13 @@ sleep 30
# Run E2E tests - 3 chances to succeed for flake tolerance
for flavor in "ios" "android" "macos"; do
for i in {1..3}; do
- echo "Running $flavor E2E test run attempt $i..."
- if ! yarn tests:"$flavor":test; then
+ e2e_log="${TMP_DIR}/tests:${flavor}:test.attempt${i}.log"
+ echo "Running $flavor E2E test run attempt $i... (log: $e2e_log)"
+ if ! yarn tests:"$flavor":test > "$e2e_log" 2>&1; then
+ echo "E2E attempt failed. Full log preserved at: $e2e_log"
+ cat "$e2e_log"
if [ $i -eq 3 ]; then
- echo "$flavor E2E test failed all $i attempts.";
+ echo "$flavor E2E test failed all $i attempts. Logs preserved in: $TMP_DIR"
terminate_testing_processes
exit 1;
fi
diff --git a/tests/.babelrc b/tests/.babelrc
index cef28c9b85..8f47e44072 100644
--- a/tests/.babelrc
+++ b/tests/.babelrc
@@ -1,6 +1,12 @@
{
"presets": ["module:@react-native/babel-preset"],
"plugins": [
+ [
+ "transform-inline-environment-variables",
+ {
+ "include": ["CI"]
+ }
+ ],
[
"istanbul",
{
diff --git a/tests/.detoxrc.js b/tests/.detoxrc.js
index 1bbbbf02f2..be9388399e 100644
--- a/tests/.detoxrc.js
+++ b/tests/.detoxrc.js
@@ -27,7 +27,7 @@ module.exports = {
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
// keep in sync with android.debug.windows below, except gradlew vs gradlew.bat
build:
- 'cd android && ./gradlew assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all && cd ..',
+ 'cd android && ./gradlew-with-worker-cap.sh assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..',
reversePorts: [8080, 8081, 8090, 9000, 9099, 9199],
},
'android.debug.windows': {
@@ -35,7 +35,7 @@ module.exports = {
binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
// android.debug.windows only exists to use .bat script vs shell here:
build:
- 'cd android && .\\gradlew.bat assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all && cd ..',
+ 'cd android && .\\gradlew-with-worker-cap.bat assembleDebug assembleAndroidTest lintDebug -DtestBuildType=debug --warning-mode all --stacktrace && cd ..',
reversePorts: [8080, 8081, 8090, 9000, 9099, 9199],
},
'android.release': {
diff --git a/tests/android/gradle.properties b/tests/android/gradle.properties
index 0742458a09..2b6458b95d 100644
--- a/tests/android/gradle.properties
+++ b/tests/android/gradle.properties
@@ -11,7 +11,7 @@ org.gradle.daemon=true
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configureondemand=true
-org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
+org.gradle.jvmargs=-Xmx5120M -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
diff --git a/tests/android/gradlew-with-worker-cap.bat b/tests/android/gradlew-with-worker-cap.bat
new file mode 100644
index 0000000000..101a2717a1
--- /dev/null
+++ b/tests/android/gradlew-with-worker-cap.bat
@@ -0,0 +1,25 @@
+@echo off
+setlocal EnableDelayedExpansion
+
+if not defined GRADLE_WORKER_CAP set "GRADLE_WORKER_CAP=6"
+
+set "CPUS="
+for /f %%i in ('wmic cpu get NumberOfCores /value ^| find "="') do (
+ for /f "tokens=2 delims==" %%j in ("%%i") do set "CPUS=%%j"
+)
+
+if not defined CPUS (
+ if defined NUMBER_OF_PROCESSORS (
+ set /a CPUS=NUMBER_OF_PROCESSORS / 2
+ ) else (
+ set "CPUS=4"
+ )
+)
+
+if !CPUS! LSS 1 set "CPUS=1"
+
+set "WORKERS=!CPUS!"
+if !WORKERS! GTR %GRADLE_WORKER_CAP% set "WORKERS=%GRADLE_WORKER_CAP%"
+
+echo [gradlew-with-worker-cap] cpus=!CPUS! cap=%GRADLE_WORKER_CAP% -^> --max-workers=!WORKERS! 1>&2
+gradlew.bat --max-workers=!WORKERS! %*
diff --git a/tests/android/gradlew-with-worker-cap.sh b/tests/android/gradlew-with-worker-cap.sh
new file mode 100755
index 0000000000..50d993776d
--- /dev/null
+++ b/tests/android/gradlew-with-worker-cap.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+WORKER_CAP="${GRADLE_WORKER_CAP:-6}"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+cd "$SCRIPT_DIR"
+
+resolve_physical_cpus() {
+ local cpus
+
+ case "$(uname -s)" in
+ Darwin)
+ if cpus="$(sysctl -n hw.physicalcpu 2>/dev/null)" && [[ "$cpus" =~ ^[0-9]+$ ]] && (( cpus >= 1 )); then
+ echo "$cpus"
+ return
+ fi
+ ;;
+ Linux)
+ if command -v lscpu >/dev/null 2>&1; then
+ cpus="$(lscpu -p=CORE 2>/dev/null | grep -Ev '^#' | sort -u | wc -l | tr -d ' ')"
+ if [[ "$cpus" =~ ^[0-9]+$ ]] && (( cpus >= 1 )); then
+ echo "$cpus"
+ return
+ fi
+ local sockets cores_per_socket
+ sockets="$(lscpu 2>/dev/null | awk '/^Socket\(s\)/ {print $2; exit}')"
+ cores_per_socket="$(lscpu 2>/dev/null | awk '/^Core\(s\) per socket/ {print $4; exit}')"
+ if [[ "$sockets" =~ ^[0-9]+$ && "$cores_per_socket" =~ ^[0-9]+$ ]] && (( sockets >= 1 && cores_per_socket >= 1 )); then
+ echo $(( sockets * cores_per_socket ))
+ return
+ fi
+ fi
+ if [[ -r /proc/cpuinfo ]]; then
+ cpus="$(awk '/^physical id/ {pid=$4} /^core id/ {cid=$4; k=pid","cid; if (!seen[k]++) n++} END {print n+0}' /proc/cpuinfo)"
+ if [[ "$cpus" =~ ^[0-9]+$ ]] && (( cpus >= 1 )); then
+ echo "$cpus"
+ return
+ fi
+ fi
+ ;;
+ esac
+
+ # Fallback: assume SMT — use half of logical CPUs
+ local logical=4
+ if command -v getconf >/dev/null 2>&1; then
+ logical="$(getconf _NPROCESSORS_ONLN)"
+ elif [[ -r /proc/cpuinfo ]]; then
+ logical="$(grep -c ^processor /proc/cpuinfo)"
+ fi
+ echo $(( logical / 2 > 0 ? logical / 2 : 1 ))
+}
+
+cpus="$(resolve_physical_cpus)"
+workers=$(( cpus < WORKER_CAP ? cpus : WORKER_CAP ))
+(( workers >= 1 )) || workers=1
+
+echo "[gradlew-with-worker-cap] cpus=$cpus cap=$WORKER_CAP -> --max-workers=$workers" >&2
+exec ./gradlew --max-workers="$workers" "$@"
diff --git a/tests/e2e/firebase.test.js b/tests/e2e/firebase.test.js
index 991519a9c9..a7dbced9f4 100644
--- a/tests/e2e/firebase.test.js
+++ b/tests/e2e/firebase.test.js
@@ -25,12 +25,19 @@ const JET_REMOTE_PORT = parseInt(process.env.JET_REMOTE_PORT || '8090', 10);
const METRO_PORT = parseInt(process.env.JET_METRO_PORT || process.env.RCT_METRO_PORT || '8081', 10);
const LAUNCH_APP_TIMEOUT_MS = parseInt(process.env.RNFB_LAUNCH_APP_TIMEOUT_MS || '180000', 10);
const LAUNCH_APP_MAX_ATTEMPTS = parseInt(process.env.RNFB_LAUNCH_APP_MAX_ATTEMPTS || '2', 10);
+const REBOOT_IOS_SIMULATOR_TIMEOUT_MS = parseInt(
+ process.env.RNFB_REBOOT_IOS_SIMULATOR_TIMEOUT_MS || String(12 * 60 * 1000),
+ 10,
+);
const JET_RETRYABLE_WS_RE = /\[jet-ws\] RETRYABLE_DISCONNECT code=(1006|1001)\b/;
const JET_RECONNECT_RECOVERED_RE = /\[jet-ws\] reconnect_recovered code=(1006|1001)\b/;
const JET_SERVER_NOT_RUNNING_RE = /server wasn't running/i;
const JET_COVERAGE_LOST_RE = /Coverage summary:[\s\S]*?Unknown% \( 0\/0 \)/;
const RETRYABLE_LAUNCH_RE =
- /launchApp timed out|RCTJavaScriptDidFailToLoad|packager-probe|Metro not responding/i;
+ /launchApp timed out|RCTJavaScriptDidFailToLoad|packager-probe|Metro not responding|Unknown application display identifier|Simulator device failed to launch/i;
+const PORT_CLOSED_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'EPIPE']);
+
+let cachedUsesLiveMetro;
function resolveDetoxConfigurationName() {
if (process.env.DETOX_CONFIGURATION) {
@@ -55,6 +62,10 @@ function resolveAppBinaryPath() {
}
function usesLiveMetro() {
+ if (cachedUsesLiveMetro !== undefined) {
+ return cachedUsesLiveMetro;
+ }
+
const configName = resolveDetoxConfigurationName();
if (/debug/i.test(configName)) {
return true;
@@ -74,6 +85,40 @@ function usesLiveMetro() {
return false;
}
+function cacheUsesLiveMetro() {
+ cachedUsesLiveMetro = usesLiveMetro();
+ console.log(`[rnfb-e2e] cached usesLiveMetro=${cachedUsesLiveMetro}`);
+}
+
+function rebootIosSimulator(testsDir) {
+ return new Promise((resolve, reject) => {
+ const repoRoot = path.resolve(testsDir, '..');
+ const bootScript = path.join(repoRoot, '.github/workflows/scripts/boot-simulator.sh');
+ console.warn(`[rnfb-e2e] Rebooting iOS simulator via ${bootScript}`);
+ const child = spawn('bash', [bootScript], {
+ cwd: repoRoot,
+ stdio: 'inherit',
+ });
+ const timer = setTimeout(() => {
+ child.kill('SIGTERM');
+ reject(new Error(`boot-simulator.sh timed out after ${REBOOT_IOS_SIMULATOR_TIMEOUT_MS}ms`));
+ }, REBOOT_IOS_SIMULATOR_TIMEOUT_MS);
+
+ child.on('close', code => {
+ clearTimeout(timer);
+ if (code === 0) {
+ resolve();
+ return;
+ }
+ reject(new Error(`boot-simulator.sh failed with code ${code}`));
+ });
+ child.on('error', err => {
+ clearTimeout(timer);
+ reject(err);
+ });
+ });
+}
+
function waitForTcpPort(port, host = '127.0.0.1', timeoutMs = 120000) {
const start = Date.now();
@@ -99,6 +144,49 @@ function waitForTcpPort(port, host = '127.0.0.1', timeoutMs = 120000) {
});
}
+function waitForTcpPortClosed(port, host = '127.0.0.1', timeoutMs = 120000) {
+ const start = Date.now();
+ let probes = 0;
+
+ return new Promise((resolve, reject) => {
+ const probe = () => {
+ if (Date.now() - start > timeoutMs) {
+ reject(
+ new Error(
+ `Timed out waiting for ${host}:${port} to close after ${timeoutMs}ms (probes=${probes})`,
+ ),
+ );
+ return;
+ }
+
+ probes += 1;
+ const socket = net.connect(port, host);
+ socket.once('connect', () => {
+ socket.end();
+ setTimeout(probe, 250);
+ });
+ socket.once('error', err => {
+ socket.destroy();
+ const elapsedMs = Date.now() - start;
+ const code = err?.code || 'UNKNOWN';
+ if (PORT_CLOSED_ERROR_CODES.has(code)) {
+ console.log(
+ `[rnfb-e2e] port ${host}:${port} closed (code=${code}, elapsed=${elapsedMs}ms, probes=${probes})`,
+ );
+ resolve();
+ return;
+ }
+ console.warn(
+ `[rnfb-e2e] port-probe non-close error code=${code} host=${host} port=${port} probe=${probes}`,
+ );
+ setTimeout(probe, 250);
+ });
+ };
+
+ probe();
+ });
+}
+
function isRetryableJetDisconnect(output) {
return JET_RETRYABLE_WS_RE.test(output);
}
@@ -240,7 +328,7 @@ function runJetE2eAttempt(attempt) {
process.stderr.write(text);
});
- return new Promise(async (resolve, reject) => {
+ const exitPromise = new Promise((resolve, reject) => {
jetProcess.on('error', err => {
err.jetOutput = output;
reject(err);
@@ -255,30 +343,37 @@ function runJetE2eAttempt(attempt) {
}
resolve({ output });
});
+ });
- try {
- console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for port ${JET_REMOTE_PORT}`);
- await waitForTcpPort(JET_REMOTE_PORT);
- if (usesLiveMetro()) {
- console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for Metro on port ${METRO_PORT}`);
- await waitForMetro(METRO_PORT);
- } else {
- console.log(
- `[rnfb-e2e] Jet attempt ${attempt}: skipping Metro wait (configuration=${resolveDetoxConfigurationName() || 'unknown'}, binary=${resolveAppBinaryPath() || 'unknown'})`,
- );
- }
- console.log(`[rnfb-e2e] Jet attempt ${attempt}: launching app`);
- await launchAppWithRetry({
- detoxURLBlacklistRegex: `.*`,
- // Avoid sync/idling blocking the main queue while Detox WS login is pending.
- detoxEnableSynchronization: 'NO',
- });
- } catch (err) {
- jetProcess.kill();
- err.jetOutput = output;
- reject(err);
+ const orchestrate = async () => {
+ console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for port ${JET_REMOTE_PORT}`);
+ await waitForTcpPort(JET_REMOTE_PORT);
+ if (usesLiveMetro()) {
+ console.log(`[rnfb-e2e] Jet attempt ${attempt}: waiting for Metro on port ${METRO_PORT}`);
+ await waitForMetro(METRO_PORT);
+ } else {
+ console.log(
+ `[rnfb-e2e] Jet attempt ${attempt}: skipping Metro wait (configuration=${resolveDetoxConfigurationName() || 'unknown'}, binary=${resolveAppBinaryPath() || 'unknown'})`,
+ );
}
- });
+ console.log(`[rnfb-e2e] Jet attempt ${attempt}: launching app`);
+ await launchAppWithRetry({
+ detoxURLBlacklistRegex: `.*`,
+ // Avoid sync/idling blocking the main queue while Detox WS login is pending.
+ detoxEnableSynchronization: 'NO',
+ });
+ };
+
+ return Promise.race([
+ orchestrate()
+ .then(() => exitPromise)
+ .catch(err => {
+ jetProcess.kill();
+ err.jetOutput = output;
+ throw err;
+ }),
+ exitPromise,
+ ]);
}
describe('Jet Tests', function () {
@@ -289,6 +384,8 @@ describe('Jet Tests', function () {
const deviceId = detox.device.id;
const testsDir = path.resolve(__dirname, '..');
+ cacheUsesLiveMetro();
+
let lastFailure;
for (let attempt = 1; attempt <= 2; attempt++) {
@@ -304,10 +401,18 @@ describe('Jet Tests', function () {
} else if (isRetryableLaunchFailure(lastFailure)) {
console.warn('[rnfb-e2e] Retrying after Metro/bundle load launch failure');
}
- try {
- await device.terminateApp();
- } catch (_) {
- // No-op
+ console.log(
+ `[rnfb-e2e] Jet attempt ${attempt}: waiting for port ${JET_REMOTE_PORT} to close before retry`,
+ );
+ await waitForTcpPortClosed(JET_REMOTE_PORT);
+ if (platform === 'ios' && process.platform === 'darwin') {
+ await rebootIosSimulator(testsDir);
+ } else {
+ try {
+ await device.terminateApp();
+ } catch (_) {
+ // No-op
+ }
}
}
diff --git a/tests/globals.js b/tests/globals.js
index 948e359261..d4d14e15fc 100644
--- a/tests/globals.js
+++ b/tests/globals.js
@@ -493,6 +493,6 @@ global.jet = {
};
// some tests flake in CI but we still run them locally
-global.isCI = process.env.CI === true;
+global.isCI = process.env.CI === 'true' || process.env.CI === true;
// Used to tell our internals that we are running tests.
globalThis.RNFBTest = true;
diff --git a/tests/package.json b/tests/package.json
index 1851c3d855..d3ee7d7697 100644
--- a/tests/package.json
+++ b/tests/package.json
@@ -45,6 +45,7 @@
"@react-native/metro-config": "^0.78.3",
"assert": "^2.1.0",
"axios": "^1.15.2",
+ "babel-plugin-transform-inline-environment-variables": "^0.4.4",
"cpy-cli": "^7.0.0",
"detox": "patch:detox@npm%3A20.51.0#~/.yarn/patches/detox-npm-20.51.0-3e13b6e309.patch",
"firebase": "^12.13.0",
diff --git a/yarn.lock b/yarn.lock
index ed8cf17af5..e14ff1b12c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8765,6 +8765,13 @@ __metadata:
languageName: node
linkType: hard
+"babel-plugin-transform-inline-environment-variables@npm:^0.4.4":
+ version: 0.4.4
+ resolution: "babel-plugin-transform-inline-environment-variables@npm:0.4.4"
+ checksum: 10/fa361287411301237fd8ce332aff4f8e8ccb8db30e87a2ddc7224c8bf7cd792eda47aca24dc2e09e70bce4c027bc8cbe22f4999056be37a25d2472945df21ef5
+ languageName: node
+ linkType: hard
+
"babel-preset-current-node-syntax@npm:^1.0.0, babel-preset-current-node-syntax@npm:^1.2.0":
version: 1.2.0
resolution: "babel-preset-current-node-syntax@npm:1.2.0"
@@ -19554,13 +19561,13 @@ __metadata:
"mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch":
version: 1.13.2
- resolution: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch::version=1.13.2&hash=5b470d"
+ resolution: "mocha-remote-server@patch:mocha-remote-server@npm%3A1.13.2#~/.yarn/patches/mocha-remote-server-npm-1.13.2-619a29d2e3.patch::version=1.13.2&hash=9f1e51"
dependencies:
debug: "npm:^4.3.4"
flatted: "npm:^3.3.1"
mocha-remote-common: "npm:1.13.2"
ws: "npm:^8.17.1"
- checksum: 10/c5861226362636fac484237e3653e6442c88a7b0b12b240ab33482961b09b5c2a1a54190c03237b2c2274616be6fe7e5c8484b355a00bb28ee401cb7b81351b6
+ checksum: 10/026256e831efc672c04312c7083927de218b2cf911eb7710abbc3c9ccb76730241eefc5bfb1f93873120244c80cb29cbd447a7edfa99bc2ce8fdc937ddc15c8c
languageName: node
linkType: hard
@@ -22318,6 +22325,7 @@ __metadata:
"@react-native/metro-config": "npm:^0.78.3"
assert: "npm:^2.1.0"
axios: "npm:^1.15.2"
+ babel-plugin-transform-inline-environment-variables: "npm:^0.4.4"
cpy-cli: "npm:^7.0.0"
detox: "patch:detox@npm%3A20.51.0#~/.yarn/patches/detox-npm-20.51.0-3e13b6e309.patch"
firebase: "npm:^12.13.0"