Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ jobs:
- uses: ./.github/actions/setup

- name: Install ktlint
env:
KTLINT_VERSION: '1.8.0'
run: |
curl -sSLO https://github.com/pinterest/ktlint/releases/latest/download/ktlint
curl -sSLO "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint"
chmod +x ktlint
sudo mv ktlint /usr/local/bin/

Expand Down
4 changes: 2 additions & 2 deletions apps/demo/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2019,7 +2019,7 @@ PODS:
- React-timing
- React-utils
- SocketRocket
- React-Sandbox (0.4.1):
- React-Sandbox (0.6.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2451,7 +2451,7 @@ SPEC CHECKSUMS:
React-runtimeexecutor: 17c70842d5e611130cb66f91e247bc4a609c3508
React-RuntimeHermes: 3c88e6e1ea7ea0899dcffc77c10d61ea46688cfd
React-runtimescheduler: 024500621c7c93d65371498abb4ee26d34f5d47d
React-Sandbox: 9c091813e335735668c62b2d3dbeb1456f93d8a5
React-Sandbox: 02d1aa294c252e37f0de3a33f2603be0314dba7a
React-timing: c3c923df2b86194e1682e01167717481232f1dc7
React-utils: 9154a037543147e1c24098f1a48fc8472602c092
ReactAppDependencyProvider: afd905e84ee36e1678016ae04d7370c75ed539be
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ class SandboxReactNativeDelegate(
return module
}
}
Log.w(TAG, "Substitution target '$resolvedName' not found in any package for '$name'")
Log.d(TAG, "Substitution target '$resolvedName' not found in any package for '$name'")
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ class SandboxReactNativeView(
var pendingComponentName: String? = null
var pendingInitialProperties: Bundle? = null
var pendingLaunchOptions: Bundle? = null
var loadScheduled: Boolean = false
var needsLoad: Boolean = false
var onAttachLoadCallback: (() -> Unit)? = null
internal var loadScheduled: Boolean = false
internal var needsLoad: Boolean = false
internal var onAttachLoadCallback: (() -> Unit)? = null

override fun onAttachedToWindow() {
super.onAttachedToWindow()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "ISandboxDelegate.h"
#include "SandboxBindingsInstaller.h"
#include "SandboxLogBox.h"
#include "SandboxRegistry.h"

#include <android/log.h>
Expand Down Expand Up @@ -376,6 +377,8 @@ jlong installSandboxJSIBindings(
LOGW("Failed to setup error handler: %s", e.what());
}

rnsandbox::disableFuseboxLogBoxToast(runtime);

// Register in C++ SandboxRegistry if origin is set
{
JNIEnv* jniEnv = getJNIEnv();
Expand Down Expand Up @@ -500,6 +503,14 @@ Java_io_callstack_rnsandbox_SandboxJSIInstaller_nativeInstallErrorHandler(
} catch (const std::exception& e) {
LOGW("Failed to setup error handler post-bundle: %s", e.what());
}

try {
// Redundant with the pre-bundle call in installSandboxJSIBindings,
// kept as a safety net for edge cases where the flag gets re-set.
rnsandbox::disableFuseboxLogBoxToast(*state->runtime);
} catch (const std::exception& e) {
LOGW("Failed to disable LogBox: %s", e.what());
}
}

JNIEXPORT void JNICALL
Expand Down
3 changes: 1 addition & 2 deletions packages/react-native-sandbox/cxx/ISandboxDelegate.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#pragma once

#include <memory>
#include <set>
#include <string>

Expand Down Expand Up @@ -56,4 +55,4 @@ class ISandboxDelegate {
virtual void setAllowedTurboModules(const std::set<std::string>& modules) = 0;
};

} // namespace rnsandbox
} // namespace rnsandbox
2 changes: 1 addition & 1 deletion packages/react-native-sandbox/cxx/SandboxDelegateWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ class SandboxDelegateWrapper : public ISandboxDelegate {
SandboxReactNativeDelegate* delegate_;
};

} // namespace rnsandbox
} // namespace rnsandbox
24 changes: 24 additions & 0 deletions packages/react-native-sandbox/cxx/SandboxLogBox.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <jsi/jsi.h>

namespace rnsandbox {

// Clear the __FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__ runtime global so that
// LogBoxData.addLog() won't create the "Open debugger to view warnings."
// migration toast inside sandboxed React Native instances.
//
// Must run BEFORE bundle evaluation — the flag is set by
// RuntimeTarget::installConsoleHandler during runtime init, and read by
// LogBoxData.addLog() as soon as the first console.warn fires.
inline void disableFuseboxLogBoxToast(facebook::jsi::Runtime& runtime) {
facebook::jsi::Object global = runtime.global();
if (global.hasProperty(runtime, "__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__")) {
global.setProperty(
runtime,
"__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__",
facebook::jsi::Value::undefined());
}
}

} // namespace rnsandbox
1 change: 0 additions & 1 deletion packages/react-native-sandbox/cxx/SandboxRegistry.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#include "SandboxRegistry.h"
#include <algorithm>
#include <iostream>

namespace rnsandbox {

Expand Down
10 changes: 10 additions & 0 deletions packages/react-native-sandbox/ios/SandboxReactNativeDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "ISandboxAwareModule.h"
#import "RCTSandboxAwareModule.h"
#include "SandboxDelegateWrapper.h"
#include "SandboxLogBox.h"
#include "SandboxRegistry.h"
#import "StubTurboModuleCxx.h"

Expand Down Expand Up @@ -339,6 +340,15 @@ - (void)hostDidStart:(RCTHost *)host
facebook::react::defineReadOnlyGlobal(runtime, "postMessage", [self createPostMessageFunction:runtime]);
facebook::react::defineReadOnlyGlobal(runtime, "setOnMessage", [self createSetOnMessageFunction:runtime]);
[self setupErrorHandler:runtime];
// Must run post-bundle (in the buffered executor) because:
// 1. installConsoleHandler sets __FUSEBOX = true during runtime init
// 2. didInitializeRuntime: fires BEFORE installConsoleHandler finishes
// 3. So clearing in didInitializeRuntime: is a no-op — Fusebox re-sets it
// 4. The buffered executor flushes AFTER bundle eval, guaranteeing the
// flag is cleared after Fusebox sets it.
// For warnings during bundle eval, sandbox JS should call
// LogBox.ignoreAllLogs() or LogBox.uninstall() to prevent the toast.
rnsandbox::disableFuseboxLogBoxToast(runtime);
}];
}

Expand Down
3 changes: 3 additions & 0 deletions packages/react-native-sandbox/react-native.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ module.exports = {
platforms: {
android: {
componentDescriptors: ['SandboxReactNativeViewComponentDescriptor'],
// Disable autolinking's default CMake integration — the JNI library
// is built by the app-level CMakeLists.txt via the codegen pipeline,
// so an additional cmake target from autolinking would conflict.
cmakeListsPath: null,
},
},
Expand Down
1 change: 0 additions & 1 deletion packages/react-native-sandbox/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ const SANDBOX_TURBOMODULES_WHITELIST = [
'AccessibilityManager',
'LinkingManager',
'BlobModule',
'LogBox',
'Appearance',
'ReactDevToolsRuntimeSettingsModule',
'NativeReactNativeFeatureFlagsCxx',
Expand Down
1 change: 0 additions & 1 deletion packages/react-native-sandbox/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ target_link_libraries(${TEST_EXECUTABLE_NAME}
target_compile_options(${TEST_EXECUTABLE_NAME} PRIVATE
-Wall
-Wextra
-std=c++17
)

enable_testing()
Expand Down
69 changes: 69 additions & 0 deletions packages/react-native-sandbox/tests/SandboxRegistryTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,75 @@ TEST_F(SandboxRegistryTest, EdgeCases) {
EXPECT_FALSE(registry.isPermittedFrom("test", ""));
}

TEST_F(SandboxRegistryTest, AllowedOriginsOverwriteOnReRegistration) {
auto& registry = SandboxRegistry::getInstance();
auto delegate1 = std::make_shared<StrictMock<MockSandboxDelegate>>();
auto delegate2 = std::make_shared<StrictMock<MockSandboxDelegate>>();

std::set<std::string> firstOrigins = {"allowed-a"};
registry.registerSandbox("shared-origin", delegate1, firstOrigins);
EXPECT_TRUE(registry.isPermittedFrom("shared-origin", "allowed-a"));
EXPECT_FALSE(registry.isPermittedFrom("shared-origin", "allowed-b"));

std::set<std::string> secondOrigins = {"allowed-b"};
registry.registerSandbox("shared-origin", delegate2, secondOrigins);

// Second registration overwrites the allowedOrigins for the whole origin
EXPECT_FALSE(registry.isPermittedFrom("shared-origin", "allowed-a"));
EXPECT_TRUE(registry.isPermittedFrom("shared-origin", "allowed-b"));

// Both delegates are still registered
auto all = registry.findAll("shared-origin");
EXPECT_EQ(all.size(), 2u);
}

TEST_F(SandboxRegistryTest, UnregisterDelegateWithUnknownDelegateIsNoOp) {
auto& registry = SandboxRegistry::getInstance();
auto registered = std::make_shared<StrictMock<MockSandboxDelegate>>();
auto unknown = std::make_shared<StrictMock<MockSandboxDelegate>>();

std::set<std::string> allowedOrigins = {"other"};
registry.registerSandbox("origin", registered, allowedOrigins);

// Unregistering a delegate that was never registered should be a no-op
registry.unregisterDelegate("origin", unknown);

auto all = registry.findAll("origin");
EXPECT_EQ(all.size(), 1u);
EXPECT_EQ(all[0], registered);

// Unregistering from a non-existent origin should also be a no-op
registry.unregisterDelegate("nonexistent", registered);
}

TEST_F(SandboxRegistryTest, FindAllWithNonExistentOrigin) {
auto& registry = SandboxRegistry::getInstance();

auto result = registry.findAll("never-registered");
EXPECT_TRUE(result.empty());
}

TEST_F(SandboxRegistryTest, ResetReleasesSharedPtrReferences) {
auto& registry = SandboxRegistry::getInstance();
auto delegate = std::make_shared<StrictMock<MockSandboxDelegate>>();

std::set<std::string> allowedOrigins = {"other"};
registry.registerSandbox("origin-a", delegate, allowedOrigins);

// Registry holds a reference, so use_count should be > 1
EXPECT_GT(delegate.use_count(), 1);

registry.reset();

// After reset, the registry should have released its reference
EXPECT_EQ(delegate.use_count(), 1);

// Registry should be empty
EXPECT_EQ(registry.find("origin-a"), nullptr);
EXPECT_TRUE(registry.findAll("origin-a").empty());
EXPECT_FALSE(registry.isPermittedFrom("origin-a", "other"));
}

TEST_F(SandboxRegistryTest, ThreadSafety) {
auto& registry = SandboxRegistry::getInstance();
const int numThreads = 4;
Expand Down
Loading