react-native-bingle-jsi
v0.2.7
Published
React Native module for Bingle peer-to-peer messaging via JSI/uniffi
Maintainers
Readme
Bingle JSI — React Native Module
React Native native module for Bingle peer-to-peer messaging, built with Rust and uniffi proc macros.
The Rust crate (bingle_jsi) exposes the full Bingle API over uniffi.
Platform-specific build scripts cross-compile the crate and generate
native bindings (Swift for iOS, Kotlin for Android). A TypeScript layer
provides type definitions for the React Native side.
Directory Layout
bingle_jsi/
├── src/ # Rust source (uniffi crate)
│ ├── lib.rs
│ └── api/
│ ├── mod.rs
│ ├── types.rs # uniffi Records & Enums
│ ├── error.rs # BingleJsiError
│ ├── callback.rs # MessageCallback trait
│ ├── bingle_jsi_api.rs # BingleJsiApi trait (20 methods)
│ └── bingle_jsi_api_impl.rs# Concrete implementation + create_bingle_api()
├── scripts/
│ ├── build_ios.sh # iOS cross-compile + XCFramework
│ ├── build_android.sh # Android cross-compile + jniLibs
│ ├── run_ios_bridge_tests.sh # run Layer 2 Swift XCTests headlessly
│ ├── add_swift_test_target.rb # one-time: add BingleJsiBridgeTests Xcode target
│ └── fix_test_target_settings.rb # one-time: configure test target build settings
├── ios/
│ ├── generated/ # (created by build_ios.sh) Swift + C headers
│ ├── BingleJsi.xcframework/ # (created by build_ios.sh)
│ ├── BingleJsiBridge.swift # React Native ↔ uniffi bridge
│ └── BingleJsiBridge.m # Objective-C bridge method declarations
├── example/
│ └── ios/
│ └── BingleJsiBridgeTests/ # Swift XCTest target (Layer 2 tests)
│ ├── MockBingleJsiApi.swift # mock BingleJsiApiProtocol
│ └── BingleJsiBridgeTests.swift # 17 test cases
├── android/
│ ├── build.gradle # Android library configuration
│ ├── generated/ # (created by build_android.sh) Kotlin bindings
│ └── src/main/jniLibs/ # (created by build_android.sh) .so files
├── ts/
│ ├── NativeBingleJsi.ts # TypeScript type definitions
│ └── index.ts # Module entry point (re-exports)
├── cpp/ # (reserved for C++ JSI bridge if needed)
├── tests/ # Rust tests
├── Cargo.toml # Rust crate configuration
├── package.json # NPM package configuration
├── bingle_jsi.podspec # CocoaPods spec for iOS
└── README.md # This filePrerequisites
Common
- Rust (via rustup) — stable toolchain
- uniffi-bindgen — provided as a binary target in the
bingle_jsicrate (src/bin/uniffi-bindgen.rs). No separate install is needed; the build scripts invoke it automatically viacargo run -p bingle_jsi --bin uniffi-bindgen.
iOS
- macOS with Xcode (command-line tools installed)
- swiftformat (installed via Homebrew) — used by
uniffi-bindgento auto-format generated Swift bindings. Install with:brew install swiftformat - Rust iOS targets (installed automatically by the build script):
aarch64-apple-ios(device)aarch64-apple-ios-sim(Apple Silicon simulator)x86_64-apple-ios(Intel simulator, optional)
Android
- ktlint (installed via Homebrew) — used by
uniffi-bindgento auto-format generated Kotlin bindings. Install with:brew install ktlint - Android NDK — set
ANDROID_NDK_HOMEor install via Android Studio SDK Manager (the script auto-detects fromANDROID_HOMEor~/Library/Android/sdk/ndk/) - Rust Android targets (installed automatically by the build script):
aarch64-linux-android(arm64-v8a)armv7-linux-androideabi(armeabi-v7a)x86_64-linux-android(x86_64)
Building
iOS
From the project root:
bash bingle_jsi/scripts/build_ios.shThis will:
- Cross-compile the
bingle_jsicrate for iOS device and simulator targets - Generate Swift bindings and a C header via
uniffi-bindgen - Create a universal simulator library (lipo) if x86_64 target is available
- Package everything into
bingle_jsi/ios/BingleJsi.xcframework
Output:
bingle_jsi/ios/BingleJsi.xcframework— static library XCFrameworkbingle_jsi/ios/generated/— Swift bindings (.swift) and C header (.h)
Android
From the project root:
bash bingle_jsi/scripts/build_android.shThis will:
- Cross-compile the
bingle_jsicrate for arm64-v8a, armeabi-v7a, and x86_64 - Copy the shared libraries (
.so) intoandroid/src/main/jniLibs/ - Generate Kotlin bindings via
uniffi-bindgen
Output:
bingle_jsi/android/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64}/libbingle_jsi.sobingle_jsi/android/generated/— Kotlin bindings
Both Platforms
cd bingle_jsi
npm run buildOr individually:
npm run build:ios
npm run build:androidUsing in a React Native Application
Step 1: Add the Module
Add react-native-bingle-jsi as a dependency. For a local development
setup, use a file reference in your React Native app's package.json:
{
"dependencies": {
"react-native-bingle-jsi": "file:../path/to/bingle_jsi"
}
}Then run:
npm installStep 2: Build Native Libraries
Before the first build (and after any Rust code changes), run the platform build scripts:
# From the bingle_jsi directory (or project root)
bash bingle_jsi/scripts/build_ios.sh # for iOS
bash bingle_jsi/scripts/build_android.sh # for AndroidStep 3: iOS — Pod Install
cd ios
pod install
cd ..The bingle_jsi.podspec links the XCFramework and Swift bindings
automatically.
Step 4: Android — Register the Package
The android/build.gradle in the module configures jniLibs and Kotlin
bindings. You must register BingleJsiPackage in your app's
MainApplication.java (or .kt):
import com.bingle.jsi.BingleJsiPackage
// In getPackages():
packages.add(BingleJsiPackage())Then sync your Gradle project in Android Studio or run:
cd android
./gradlew sync
cd ..Step 5: Use in TypeScript
All native methods are async (Promise-based) because they cross the React Native bridge.
import {
BingleJsi,
initBingleJsi,
BingleJsiConfig,
BingleMessage,
ContactSource,
} from 'react-native-bingle-jsi';
// Initialize — must be called before any other method
const config: BingleJsiConfig = {
handle: 'alice',
passphrase: null,
relay: false,
static_ip: null,
stun_servers: null,
stun_servers_file: null,
node_file: '/path/to/node.json',
log_level: 'info',
app_id: 12345,
asset_id: 67890,
handle_cache_expiry_secs: 300,
debug: false,
local: '/path/to/local_state.json',
};
await initBingleJsi(config);
// Get version info
const version = await BingleJsi.version();
console.log(`Bingle v${version.version}`);
// Send a message
const msg: BingleMessage = {
app: null,
type: null,
tag: null,
response_tag: null,
text: 'Hello from React Native!',
data: null,
};
await BingleJsi.sendMessageToHandle('bob', msg);
// Local API (when `local` is set in config)
const keypair = await BingleJsi.generateKeypair();
console.log(`Generated keypair: ${keypair.id}`);
await BingleJsi.addContact('bob', 'BOB_ALGO_ADDRESS', ContactSource.Manual);
const contacts = await BingleJsi.getContacts();Native Module Architecture
The module uses a two-layer architecture:
Rust → uniffi bindings: The
bingle_jsiRust crate is compiled to a static library (iOS) or shared library (Android).uniffi-bindgengenerates Swift and Kotlin bindings that call the Rust FFI layer.uniffi bindings → React Native bridge: Platform-specific bridge classes wrap the uniffi-generated API and register it as a React Native native module named
"BingleJsi":- iOS:
ios/BingleJsiBridge.swift+ios/BingleJsiBridge.m(extendsRCTEventEmitter, registered viaRCT_EXTERN_MODULE) - Android:
android/src/main/java/com/bingle/jsi/BingleJsiModule.ktBingleJsiPackage.kt(extendsReactContextBaseJavaModule, registered viaReactPackage)
- iOS:
TypeScript:
ts/index.tsacquires the native module viaNativeModules.BingleJsiand exportsinitBingleJsi()plus the typedBingleJsiproxy object.
All bridge methods are Promise-based — they dispatch work to a background thread and resolve/reject via the React Native bridge.
Running Rust Tests
The Rust crate has its own test suite:
# Run bingle_jsi tests only
cargo test -p bingle_jsi
# Run all workspace tests
cargo testiOS Bridge Tests (Layer 2 — Swift XCTest)
The Swift bridge (BingleJsiBridge.swift) is tested independently of the
real Bingle engine using a mock implementation of BingleJsiApiProtocol.
This lets the full call path through the bridge be exercised — including
Promise resolution, error propagation, and the "not initialized" rejection
path — without a passphrase, a live network, or a running Bingle node.
How it works
BingleJsiBridge.swift holds its API instance as (any BingleJsiApiProtocol)?
rather than the concrete BingleJsiApi type. A package-internal
injectApi(_ api:) method allows a test-supplied mock to be injected
before any test case runs.
The test target (BingleJsiBridgeTests) lives in:
bingle_jsi/example/ios/BingleJsiBridgeTests/
├── MockBingleJsiApi.swift # mock implementing all protocol methods
└── BingleJsiBridgeTests.swift # 24 XCTest casesThe mock records every call, exposes configurable return values, and can be made to throw on demand — enabling both happy-path and error-path coverage.
A SpyBingleJsiBridge subclass overrides sendEvent(withName:body:) to
capture React Native events in memory, allowing tests to assert the exact
event name and payload delivered to the JS layer.
Tests covered:
| Test | What it exercises |
|------|-------------------|
| testHandleLookup_resolvesWithExpectedUserId | successful handle → user-id lookup |
| testHandleLookup_rejectsOnApiError | bridge rejects when mock throws |
| testHandleLookup_notInitialized_rejectsWithCorrectCode | rejection before initialize |
| testSendMessageToId_resolvesAndRecordsCall | message forwarded to mock, bool result resolved |
| testSendMessageToId_mapsAllMessageFields | all message fields (including cipher_suite) mapped correctly |
| testSendMessageToId_rejectsOnApiError | bridge rejects when mock throws |
| testSendMessageToId_notInitialized_rejectsWithCorrectCode | rejection before initialize |
| testSendMessageToHandle_resolvesAndRecordsCall | handle-based send forwarded to mock |
| testVersion_resolvesWithVersionInfo | VersionInfo fields returned via Promise |
| testIsStarted_resolvesWithTrueWhenStarted | true resolved when mock returns started |
| testIsStarted_resolvesWithFalseWhenNotStarted | false resolved when mock returns not started |
| testStart_callsApiStart | start() forwarded and resolved |
| testSetMessageCallback_registersCallback | callback registered on mock |
| testSetMessageCallback_deliversMessageToEventEmitter | inbound message fires onMessage event with correct payload |
| testKeypairStatus_resolvesWithExpectedFields | KeypairStatusResponse fields resolved |
| testGetNatType_resolvesWithNatTypeString | NatTypeResponse nat type string resolved |
| testGenerateKeypair_resolvesWithKeypairFields | Keypair id and passphrase resolved |
| testIsBlocked_resolvesWithFalseByDefault | false resolved for unblocked contact |
| testIsBlocked_resolvesWithTrueForBlockedContact | true resolved for blocked contact |
| testGetMessages_includesCipherSuite | cipher_suite present (or nil) for each returned message |
Prerequisites
- macOS with Xcode installed
- CocoaPods (
brew install cocoapods) - An iOS 18.x simulator runtime available (
xcrun simctl list runtimes) - The example workspace already pod-installed:
cd bingle_jsi/example/ios && pod install
The XCFramework (bingle_jsi/ios/BingleJsi.xcframework) must exist.
If it is missing, build it first:
bash bingle_jsi/scripts/build_ios.shRunning the tests
Use the provided script from the project root:
./bingle_jsi/scripts/run_ios_bridge_tests.shThe script will:
- Boot the
iPhone 16(iOS 18.6) simulator if it is not already running - Run
xcodebuild testagainst theBingleJsiBridgeTestsscheme - Write the full
xcodebuildlog totmp/ios_bridge_tests.log - Print a pass/fail summary and exit with code 0 (pass) or 1 (fail)
To run directly with xcodebuild (from bingle_jsi/example/ios):
xcodebuild test \
-workspace BingleJsiExample.xcworkspace \
-scheme BingleJsiBridgeTests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6' \
-sdk iphonesimulator \
-quietThe simulator runs headlessly — no display or UI interaction is required, so the tests are suitable for CI.
Project setup scripts
The Xcode test target was added to BingleJsiExample.xcodeproj using two
Ruby scripts (invoked once; do not need to be re-run unless the project
file is regenerated):
| Script | Purpose |
|--------|---------|
| bingle_jsi/scripts/add_swift_test_target.rb | Adds the BingleJsiBridgeTests target and registers source files |
| bingle_jsi/scripts/fix_test_target_settings.rb | Configures PRODUCT_NAME, deployment target, and standalone bundle settings (TEST_HOST = '') |
Both scripts use the xcodeproj gem bundled with CocoaPods and are
invoked with:
GEM_PATH=/opt/homebrew/Cellar/cocoapods/1.16.2_1/libexec \
GEM_HOME=/opt/homebrew/Cellar/cocoapods/1.16.2_1/libexec \
ruby -I /opt/homebrew/Cellar/cocoapods/1.16.2_1/libexec/gems/xcodeproj-1.27.0/lib \
bingle_jsi/scripts/<script>.rbAPI Reference
The full API is defined in src/api/bingle_jsi_api.rs. Key methods:
| Method | Description |
|--------|-------------|
| createBingleApi(config) | Initialize the API with a BingleJsiConfig |
| handleLookup(handle) | Look up a user ID by handle |
| sendMessageToId(userId, message) | Send a message to a user ID |
| sendMessageToHandle(handle, message) | Send a message to a handle |
| sendMessageToNetwork(nsk, userId, message) | Send via network endpoint |
| sendMessageTo*WithResponse(...) | Send and wait for response |
| queued() | Get all queued received messages |
| version() | Get version info |
| getNatType() | Get detected NAT type |
| generateKeypair() | Generate a new Algorand keypair |
| registerKeypair(handle) | Register keypair on-chain |
| addContact(handle, id, source) | Add a contact |
| blockContact(id) | Block a contact |
| removeContact(id) | Remove a contact |
| isBlocked(id) | Check if contact is blocked |
| getContacts() | List unblocked contacts |
| addMessage(sender, recipients, ts, text) | Store a message locally |
| getMessages() | List stored messages |
| keypairStatus() | Check keypair funding status |
| save(path) / load(path) | Persist/restore local state |
| setMessageCallback(callback) | Register incoming message callback |
