@chitchat/sdk-react-native 0.2.0-dev.2 → 0.3.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/ChitChatTcp.podspec +15 -0
  2. package/README.md +20 -39
  3. package/android/.gradle/8.9/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.9/checksums/md5-checksums.bin +0 -0
  5. package/android/.gradle/8.9/checksums/sha1-checksums.bin +0 -0
  6. package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
  7. package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
  8. package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/8.9/gc.properties +0 -0
  10. package/android/.gradle/9.2.0/checksums/checksums.lock +0 -0
  11. package/android/.gradle/9.2.0/fileChanges/last-build.bin +0 -0
  12. package/android/.gradle/9.2.0/fileHashes/fileHashes.bin +0 -0
  13. package/android/.gradle/9.2.0/fileHashes/fileHashes.lock +0 -0
  14. package/android/.gradle/9.2.0/gc.properties +0 -0
  15. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  16. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  17. package/android/.gradle/vcs-1/gc.properties +0 -0
  18. package/android/build.gradle +14 -0
  19. package/android/src/main/AndroidManifest.xml +3 -0
  20. package/android/src/main/java/com/chitchat/sdk/ChitChatTcpModule.kt +102 -0
  21. package/android/src/main/java/com/chitchat/sdk/ChitChatTcpPackage.kt +11 -0
  22. package/index.d.ts +15 -1
  23. package/ios/ChitChatTcp.h +4 -0
  24. package/ios/ChitChatTcp.mm +84 -0
  25. package/package.json +63 -10
  26. package/react-native.config.js +12 -0
  27. package/react.d.ts +5 -0
  28. package/src/index.js +35 -6
  29. package/src/protobuf-bridge.js +87 -0
  30. package/src/react.js +12 -0
@@ -0,0 +1,15 @@
1
+ require 'json'
2
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
3
+ Pod::Spec.new do |s|
4
+ s.name = 'ChitChatTcp'
5
+ s.version = package['version']
6
+ s.summary = 'ChitChat TLS TCP protobuf transport'
7
+ s.homepage = package['homepage']
8
+ s.license = { :type => 'UNLICENSED' }
9
+ s.author = 'ChitChat'
10
+ s.platforms = { :ios => '15.1' }
11
+ s.source = { :git => 'https://github.com/07Akashh/E2E-Chat.git', :tag => s.version.to_s }
12
+ s.source_files = 'ios/**/*.{h,m,mm}'
13
+ s.dependency 'React-Core'
14
+ s.dependency 'CocoaAsyncSocket', '7.6.5'
15
+ end
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # ChitChat React Native SDK
2
2
 
3
+ ## 0.3.0-dev.0 source release
4
+
5
+ Adds packaged Android/iOS TLS TCP sources, the protobuf handshake adapter, createChitChatNativeCalls and @chitchat/sdk-react-native/react. TCP + protobuf remains the default; WSS is optional and never an automatic fallback.
6
+
7
+ This version is **not yet published or production-certified**. See the [calling quickstart](../docs/CALLING.md), [all-five SDK audit](../docs/GAP_AUDIT.md), [migration/support targets](../docs/RELEASE.md), and [local tarball installation](../README.md). Existing npm development tags may still resolve to older SDKs.
8
+
9
+
3
10
  `@chitchat/sdk-react-native` connects a React Native application through a native TLS TCP transport and binary Protobuf frames. It is for iOS/Android native builds; it is not a browser SDK.
4
11
 
5
12
  This package is currently a **development prerelease**. Pin the version you have tested before shipping an application.
@@ -10,11 +17,9 @@ The native SDK validates both the TLS endpoint and the linked bridge result befo
10
17
 
11
18
  ## Read this before integrating
12
19
 
13
- This JavaScript package needs a linked native bridge. You pass that bridge as `nativeModule`; the SDK does not secretly create TCP sockets in JavaScript.
20
+ This package now includes an autolinkable `ChitChatTcp` byte-stream module. The default `createChitChatNativeClient({ endpoint, deviceId, tokenProvider })` loads it and handles the binary protobuf handshake. Run pods and rebuild after installing. Install `react-native-webrtc` separately for calling. Expo Go cannot load these native modules.
14
21
 
15
- For the current ChitChat app implementation, the bridge is exposed as `NativeModules.XMPPNativeModule` (with `NativeModules.XMPPNative` as a compatibility fallback). A third-party application must include the official ChitChat native bridge or implement the same documented bridge contract. The bridge must perform TLS validation, Protobuf framing, and certificate policy on iOS and Android.
16
-
17
- **Expo Go cannot load custom native modules.** Use a development build or a prebuilt/bare React Native application, link the bridge, then rebuild iOS/Android.
22
+ If reusing the ChitChat app's `XMPPNativeModule`, wrap it with `createChitChatNativeBridge`; it does not directly implement the high-level SDK connection contract. Do not let both the old app singleton and the SDK control the same native module. See the calling quickstart for both paths.
18
23
 
19
24
  ## 1. Install
20
25
 
@@ -30,43 +35,19 @@ You also need a backend session endpoint made with `@chitchat/sdk-server`. It re
30
35
  ## 2. Create the client
31
36
 
32
37
  ```js
33
- import { NativeModules } from 'react-native';
34
38
  import { createBackendTokenProvider } from '@chitchat/sdk-core';
35
- import { createChitChatNativeClient } from '@chitchat/sdk-react-native';
36
-
37
- const nativeModule = NativeModules.XMPPNativeModule || NativeModules.XMPPNative;
38
- if (!nativeModule) {
39
- throw new Error('ChitChat native bridge is not linked; rebuild a development/native app.');
40
- }
41
-
42
- const tokenProvider = createBackendTokenProvider({
43
- endpoint: 'https://api.example.com/api/chitchat/session',
44
- getHeaders: () => ({ authorization: `Bearer ${getYourAppAccessToken()}` }),
45
- getBody: () => ({ deviceId: getStableInstallationId() })
46
- });
47
-
48
- const client = createChitChatNativeClient({
49
- // TLS TCP hostname. Port 443 is applied when the port is omitted.
50
- endpoint: 'realtime.example.com',
51
- tokenProvider,
52
- nativeModule,
53
- storage: durableNativeOutbox
54
- });
55
-
56
- client.on('connected', () => setRealtimeState('connected'));
57
- client.on('message', (frame) => handleChitChatFrame(frame));
58
- client.on('error', (error) => reportNonSensitiveRealtimeError(error));
59
-
60
- client.connect().catch(reportNonSensitiveRealtimeError);
39
+ import { createChitChatNativeClient, createChitChatNativeCalls } from '@chitchat/sdk-react-native';
40
+ const tokenProvider = createBackendTokenProvider({ endpoint: 'https://your-app.example/api/session', getBody: () => ({ deviceId }) });
41
+ const realtime = createChitChatNativeClient({ endpoint: 'realtime.example.com:443', deviceId, tokenProvider });
42
+ const calls = createChitChatNativeCalls({ realtime, participantId, getIceServers });
43
+ await calls.join({ callId, participantIds });
44
+ // On logout:
45
+ await calls.dispose();
46
+ await realtime.close();
47
+ tokenProvider.invalidate();
61
48
  ```
62
49
 
63
- On logout or account switch:
64
-
65
- ```js
66
- client.close()
67
- .then(() => tokenProvider.invalidate())
68
- .catch(reportNonSensitiveRealtimeError);
69
- ```
50
+ `participantId`, `participantIds` and `callId` come from the application's approved session/roster. `getIceServers` calls its authenticated backend. See the quickstart for authorization boundaries, native permissions and rendering.
70
51
 
71
52
  ## Native bridge contract
72
53
 
@@ -122,4 +103,4 @@ The default core outbox is memory-only. To preserve queued messages when the app
122
103
 
123
104
  ## Current scope
124
105
 
125
- This package provides the React Native client lifecycle and native transport adapter boundary. Call UI, CallKit/ConnectionService, push wake-up, ringtone, WebRTC media, meetings, streaming, notifications, and agents are separate platform features and must be integrated and tested before release.
106
+ The 0.3 source release includes the calling additions described above. See the [gap audit](../docs/GAP_AUDIT.md) for supported behavior and remaining service requirements. Calling is a foreground small-mesh implementation. Server-authoritative rooms, roles, moderation, recording, signed call webhooks, SFU meetings, background native calling and device/browser production certification remain unimplemented or unverified.
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ #Sat Sep 12 14:17:30 IST 2026
2
+ gradle.version=8.9
File without changes
@@ -0,0 +1,14 @@
1
+ buildscript {
2
+ def kotlinVersion = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : '2.0.21'
3
+ repositories { google(); mavenCentral() }
4
+ dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" }
5
+ }
6
+ apply plugin: 'com.android.library'
7
+ apply plugin: 'kotlin-android'
8
+ android {
9
+ namespace 'com.chitchat.sdk'
10
+ compileSdkVersion rootProject.ext.has('compileSdkVersion') ? rootProject.ext.get('compileSdkVersion') : 35
11
+ defaultConfig { minSdkVersion 24; targetSdkVersion 35 }
12
+ }
13
+ repositories { google(); mavenCentral() }
14
+ dependencies { implementation 'com.facebook.react:react-android' }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ </manifest>
@@ -0,0 +1,102 @@
1
+ package com.chitchat.sdk
2
+
3
+ import android.util.Base64
4
+ import com.facebook.react.bridge.*
5
+ import com.facebook.react.modules.core.DeviceEventManagerModule
6
+ import java.io.DataInputStream
7
+ import java.net.InetSocketAddress
8
+ import java.nio.ByteBuffer
9
+ import java.util.concurrent.Executors
10
+ import java.util.concurrent.atomic.AtomicInteger
11
+ import javax.net.ssl.SSLParameters
12
+ import javax.net.ssl.SSLSocket
13
+ import javax.net.ssl.SSLSocketFactory
14
+
15
+ /** TLS byte stream only. Authentication and protobuf decoding belong to JS. */
16
+ class ChitChatTcpModule(private val context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {
17
+ private val lock = Any()
18
+ private var generation = 0L
19
+ private var socket: SSLSocket? = null
20
+ private val writes = Executors.newSingleThreadExecutor()
21
+ private val pendingBytes = AtomicInteger(0)
22
+ override fun getName() = "ChitChatTcp"
23
+ override fun getConstants(): Map<String, Any> = mapOf("supportsTLS" to true, "supportsProtobufFrames" to true)
24
+ @ReactMethod(isBlockingSynchronousMethod = true) fun randomUUID(): String = java.util.UUID.randomUUID().toString()
25
+ @ReactMethod fun addListener(name: String) {}
26
+ @ReactMethod fun removeListeners(count: Int) {}
27
+
28
+ private fun emit(name: String, data: WritableMap? = null) {
29
+ if (context.hasActiveReactInstance()) context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(name, data)
30
+ }
31
+ private fun current(value: Long) = synchronized(lock) { generation == value }
32
+ private fun closeConnection(notify: Boolean) {
33
+ synchronized(lock) {
34
+ generation++
35
+ try { socket?.close() } catch (_: Exception) {}
36
+ socket = null
37
+ }
38
+ if (notify) emit("onDisconnected")
39
+ }
40
+ @ReactMethod fun disconnect() = closeConnection(true)
41
+ override fun invalidate() { closeConnection(false); writes.shutdownNow(); super.invalidate() }
42
+
43
+ @ReactMethod fun connectProtobuf(host: String, port: Int, useTLS: Boolean) {
44
+ if (!useTLS || host.isBlank() || port !in 1..65535) {
45
+ emit("onError", Arguments.createMap().apply { putString("code", "INVALID_TLS_ENDPOINT") }); return
46
+ }
47
+ closeConnection(false)
48
+ val attempt = synchronized(lock) { generation }
49
+ Thread {
50
+ var connected: SSLSocket? = null
51
+ try {
52
+ val target = SSLSocketFactory.getDefault().createSocket() as SSLSocket
53
+ connected = target
54
+ synchronized(lock) {
55
+ if (generation != attempt) { target.close(); return@Thread }
56
+ socket = target
57
+ }
58
+ target.soTimeout = 15000
59
+ target.connect(InetSocketAddress(host, port), 15000)
60
+ target.sslParameters = SSLParameters().apply { endpointIdentificationAlgorithm = "HTTPS" }
61
+ target.enabledProtocols = target.supportedProtocols.filter { it == "TLSv1.2" || it == "TLSv1.3" }.toTypedArray()
62
+ target.startHandshake()
63
+ if (!current(attempt)) return@Thread
64
+ target.soTimeout = 60000
65
+ emit("onTransportOpen", Arguments.createMap().apply { putBoolean("tls", true) })
66
+ val input = DataInputStream(target.inputStream)
67
+ while (current(attempt)) {
68
+ val length = input.readInt()
69
+ if (length !in 1..1048576) throw IllegalArgumentException("Frame size")
70
+ val bytes = ByteArray(length + 4)
71
+ ByteBuffer.wrap(bytes).putInt(length)
72
+ input.readFully(bytes, 4, length)
73
+ if (current(attempt)) emit("onProtobufFrame", Arguments.createMap().apply { putString("frame", Base64.encodeToString(bytes, Base64.NO_WRAP)) })
74
+ }
75
+ } catch (_: Exception) {
76
+ if (current(attempt)) {
77
+ emit("onError", Arguments.createMap().apply { putString("code", "TLS_TRANSPORT_FAILED") })
78
+ closeConnection(true)
79
+ }
80
+ } finally { try { connected?.close() } catch (_: Exception) {} }
81
+ }.start()
82
+ }
83
+
84
+ @ReactMethod fun sendProtobufFrame(frame: String, promise: Promise) {
85
+ if (frame.length > 1398108) { promise.reject("FRAME_TOO_LARGE", "Frame exceeds limit"); return }
86
+ val bytes = try { Base64.decode(frame, Base64.NO_WRAP) } catch (_: Exception) { promise.reject("INVALID_FRAME", "Invalid binary frame"); return }
87
+ if (bytes.size !in 5..1048580 || ByteBuffer.wrap(bytes).int != bytes.size - 4) { promise.reject("INVALID_FRAME", "Invalid frame length"); return }
88
+ if (pendingBytes.addAndGet(bytes.size) > 4194320) { pendingBytes.addAndGet(-bytes.size); promise.reject("BACKPRESSURE", "Native send queue is full"); return }
89
+ val attempt = synchronized(lock) { generation }
90
+ try {
91
+ writes.execute {
92
+ try {
93
+ val target = synchronized(lock) { if (attempt == generation) socket else null } ?: throw IllegalStateException()
94
+ target.outputStream.write(bytes)
95
+ target.outputStream.flush()
96
+ promise.resolve(null)
97
+ } catch (_: Exception) { promise.reject("NATIVE_SEND_FAILED", "Secure transport write failed") }
98
+ finally { pendingBytes.addAndGet(-bytes.size) }
99
+ }
100
+ } catch (_: Exception) { pendingBytes.addAndGet(-bytes.size); promise.reject("NATIVE_CLOSED", "Native transport is closed") }
101
+ }
102
+ }
@@ -0,0 +1,11 @@
1
+ package com.chitchat.sdk
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class ChitChatTcpPackage : ReactPackage {
9
+ override fun createNativeModules(context: ReactApplicationContext): List<NativeModule> = listOf(ChitChatTcpModule(context))
10
+ override fun createViewManagers(context: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
11
+ }
package/index.d.ts CHANGED
@@ -4,4 +4,18 @@ export interface ChitChatNativeModule { connect(input: { endpoint: string; token
4
4
  export function validateNativeTlsEndpoint(endpoint: string): string;
5
5
  export function validatePinnedPublicKeyHashes(value?: string[]): string[];
6
6
  export function createNativeTcpTransportFactory(options: { nativeModule: ChitChatNativeModule; pinnedPublicKeyHashes?: string[]; minimumTlsVersion?: 'TLSv1.2' | 'TLSv1.3' }): ReliableRealtimeClientOptions['nativeTransportFactory'];
7
- export function createChitChatNativeClient(options: Omit<ReliableRealtimeClientOptions, 'runtime' | 'nativeTransportFactory'> & { nativeModule: ChitChatNativeModule; pinnedPublicKeyHashes?: string[]; minimumTlsVersion?: 'TLSv1.2' | 'TLSv1.3' }): ReliableRealtimeClient;
7
+ export function createChitChatNativeClient(options: Omit<ReliableRealtimeClientOptions, 'runtime' | 'nativeTransportFactory'> & { nativeModule?: ChitChatNativeModule; protobuf?: import('@chitchat/sdk-protocol').RealtimeEnvelopeCodec; deviceId?: string; pinnedPublicKeyHashes?: string[]; minimumTlsVersion?: 'TLSv1.2' | 'TLSv1.3' }): ReliableRealtimeClient;
8
+
9
+ export interface NativeMediaTrack { id: string; kind: string; enabled: boolean; stop(): void; }
10
+ export interface NativeMediaStream { id: string; toURL(): string; getTracks(): NativeMediaTrack[]; getAudioTracks(): NativeMediaTrack[]; getVideoTracks(): NativeMediaTrack[]; }
11
+ export type NativeCallsClient = import('@chitchat/sdk-core').CallsClient<NativeMediaStream, NativeMediaTrack>;
12
+ export function createChitChatNativeCalls(options: Omit<import('@chitchat/sdk-core').CallsOptions, 'rtc'> & { rtc?: import('@chitchat/sdk-core').WebRtcRuntime }): NativeCallsClient;
13
+ export const createChitChatNativeWssClient: typeof import('@chitchat/sdk-web').createChitChatWebClient;
14
+ export interface NativeProtobufModule {
15
+ supportsTLS: boolean;
16
+ supportsProtobufFrames: boolean;
17
+ connectProtobuf(host: string, port: number, useTLS: true): void | Promise<void>;
18
+ sendProtobufFrame(base64: string): void | Promise<void>;
19
+ disconnect(): void | Promise<void>;
20
+ }
21
+ export function createChitChatNativeBridge(options: { nativeModule: NativeProtobufModule; eventEmitter: { addListener(event: string, listener: (value: any) => void): { remove(): void } }; protobuf: import('@chitchat/sdk-protocol').RealtimeEnvelopeCodec; openTimeoutMs?: number }): ChitChatNativeModule;
@@ -0,0 +1,4 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+ @interface ChitChatTcp : RCTEventEmitter <RCTBridgeModule>
4
+ @end
@@ -0,0 +1,84 @@
1
+ #import "ChitChatTcp.h"
2
+ #import <CocoaAsyncSocket/GCDAsyncSocket.h>
3
+ #import <CFNetwork/CFNetwork.h>
4
+
5
+ @interface ChitChatTcp () <GCDAsyncSocketDelegate>
6
+ @property(nonatomic, strong) GCDAsyncSocket *socket;
7
+ @property(nonatomic, copy) NSString *host;
8
+ @property(nonatomic, strong) NSData *header;
9
+ @property(nonatomic, assign) BOOL listening;
10
+ @property(nonatomic, assign) BOOL secure;
11
+ @property(nonatomic, assign) NSUInteger pendingBytes;
12
+ @property(nonatomic, assign) long writeTag;
13
+ @property(nonatomic, strong) NSMutableDictionary *pendingWrites;
14
+ @end
15
+
16
+ @implementation ChitChatTcp
17
+ RCT_EXPORT_MODULE(ChitChatTcp)
18
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(randomUUID) { return [[NSUUID UUID] UUIDString]; }
19
+ + (BOOL)requiresMainQueueSetup { return YES; }
20
+ - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); }
21
+ - (NSDictionary *)constantsToExport { return @{ @"supportsTLS": @YES, @"supportsProtobufFrames": @YES }; }
22
+ - (NSArray<NSString *> *)supportedEvents { return @[@"onTransportOpen", @"onProtobufFrame", @"onDisconnected", @"onError"]; }
23
+ - (void)startObserving { self.listening = YES; }
24
+ - (void)stopObserving { self.listening = NO; }
25
+ - (void)emit:(NSString *)name body:(id)body { if (self.listening) [self sendEventWithName:name body:body]; }
26
+ - (void)closeSocket {
27
+ self.socket.delegate = nil; [self.socket disconnect]; self.socket = nil; self.secure = NO; self.header = nil;
28
+ for (NSDictionary *entry in self.pendingWrites.allValues) { RCTPromiseRejectBlock reject = entry[@"reject"]; reject(@"NATIVE_CLOSED", @"Native transport closed", nil); }
29
+ [self.pendingWrites removeAllObjects]; self.pendingBytes = 0;
30
+ }
31
+ - (void)invalidate { [self closeSocket]; [super invalidate]; }
32
+ RCT_EXPORT_METHOD(disconnect) { [self closeSocket]; [self emit:@"onDisconnected" body:@{}]; }
33
+ RCT_EXPORT_METHOD(connectProtobuf:(NSString *)host port:(NSInteger)port useTLS:(BOOL)useTLS) {
34
+ [self closeSocket];
35
+ if (!useTLS || !host.length || port < 1 || port > 65535) { [self emit:@"onError" body:@{@"code": @"INVALID_TLS_ENDPOINT"}]; return; }
36
+ self.host = host; self.pendingWrites = [NSMutableDictionary dictionary];
37
+ self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
38
+ NSError *error = nil;
39
+ if (![self.socket connectToHost:host onPort:(uint16_t)port withTimeout:15 error:&error]) [self emit:@"onError" body:@{@"code": @"TLS_CONNECT_FAILED"}];
40
+ }
41
+ - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
42
+ if (sock != self.socket) return;
43
+ [sock startTLS:@{ (__bridge NSString *)kCFStreamSSLPeerName: self.host,
44
+ GCDAsyncSocketSSLProtocolVersionMin: @(kTLSProtocol12) }];
45
+ }
46
+ - (void)socketDidSecure:(GCDAsyncSocket *)sock {
47
+ if (sock != self.socket) return;
48
+ self.secure = YES; [self emit:@"onTransportOpen" body:@{@"tls": @YES}];
49
+ [sock readDataToLength:4 withTimeout:60 tag:1];
50
+ }
51
+ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
52
+ if (sock != self.socket) return;
53
+ if (tag == 1) {
54
+ uint32_t length; [data getBytes:&length length:4]; length = CFSwapInt32BigToHost(length);
55
+ if (length < 1 || length > 1048576) { [sock disconnect]; return; }
56
+ self.header = data; [sock readDataToLength:length withTimeout:60 tag:2];
57
+ } else {
58
+ NSMutableData *frame = [self.header mutableCopy]; [frame appendData:data]; self.header = nil;
59
+ [self emit:@"onProtobufFrame" body:@{@"frame": [frame base64EncodedStringWithOptions:0]}];
60
+ [sock readDataToLength:4 withTimeout:60 tag:1];
61
+ }
62
+ }
63
+ RCT_EXPORT_METHOD(sendProtobufFrame:(NSString *)base64 resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
64
+ if (!self.secure || !self.socket) { reject(@"NATIVE_CLOSED", @"Secure transport is closed", nil); return; }
65
+ if (base64.length > 1398108) { reject(@"FRAME_TOO_LARGE", @"Frame exceeds limit", nil); return; }
66
+ NSData *frame = [[NSData alloc] initWithBase64EncodedString:base64 options:0];
67
+ uint32_t length = 0; if (frame.length >= 4) [frame getBytes:&length length:4];
68
+ if (frame.length < 5 || frame.length > 1048580 || CFSwapInt32BigToHost(length) != frame.length - 4) { reject(@"INVALID_FRAME", @"Invalid binary frame", nil); return; }
69
+ if (self.pendingBytes + frame.length > 4194320) { reject(@"BACKPRESSURE", @"Native send queue is full", nil); return; }
70
+ long tag = ++self.writeTag; self.pendingBytes += frame.length;
71
+ self.pendingWrites[@(tag)] = @{ @"resolve": [resolve copy], @"reject": [reject copy], @"bytes": @(frame.length) };
72
+ [self.socket writeData:frame withTimeout:15 tag:tag];
73
+ }
74
+ - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
75
+ if (sock != self.socket) return;
76
+ NSDictionary *entry = self.pendingWrites[@(tag)]; if (!entry) return;
77
+ self.pendingBytes -= [entry[@"bytes"] unsignedIntegerValue]; [self.pendingWrites removeObjectForKey:@(tag)];
78
+ RCTPromiseResolveBlock resolve = entry[@"resolve"]; resolve(nil);
79
+ }
80
+ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {
81
+ if (sock != self.socket) return;
82
+ [self closeSocket]; [self emit:@"onDisconnected" body:@{}];
83
+ }
84
+ @end
package/package.json CHANGED
@@ -1,19 +1,72 @@
1
1
  {
2
2
  "name": "@chitchat/sdk-react-native",
3
- "version": "0.2.0-dev.2",
3
+ "version": "0.3.0-dev.1",
4
4
  "description": "React Native TLS transport adapter for the ChitChat Developer Platform",
5
- "repository": { "type": "git", "url": "git+https://github.com/07Akashh/E2E-Chat.git", "directory": "developer-web/sdk/chitchat-native" },
6
- "bugs": { "url": "https://github.com/07Akashh/E2E-Chat/issues" },
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/07Akashh/E2E-Chat.git",
8
+ "directory": "developer-web/sdk/chitchat-native"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/07Akashh/E2E-Chat/issues"
12
+ },
7
13
  "homepage": "https://github.com/07Akashh/E2E-Chat/tree/main/docs/developer-platform",
8
14
  "main": "src/index.js",
9
- "exports": { ".": { "types": "./index.d.ts", "require": "./src/index.js", "import": "./src/index.js" } },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./index.d.ts",
18
+ "require": "./src/index.js",
19
+ "import": "./src/index.js"
20
+ },
21
+ "./react": {
22
+ "types": "./react.d.ts",
23
+ "require": "./src/react.js",
24
+ "import": "./src/react.js"
25
+ }
26
+ },
10
27
  "types": "index.d.ts",
11
- "files": ["src", "index.d.ts", "README.md"],
12
- "engines": { "node": ">=18" },
13
- "scripts": { "test": "node test/transport.test.js" },
14
- "dependencies": { "@chitchat/sdk-core": "0.2.0-dev.2" },
15
- "keywords": ["chitchat", "realtime", "react-native", "tls", "protobuf"],
28
+ "files": [
29
+ "src",
30
+ "index.d.ts",
31
+ "README.md",
32
+ "android",
33
+ "ios",
34
+ "ChitChatTcp.podspec",
35
+ "react-native.config.js",
36
+ "react.d.ts"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18"
40
+ },
41
+ "scripts": {
42
+ "test": "node test/transport.test.js"
43
+ },
44
+ "dependencies": {
45
+ "@chitchat/sdk-core": "0.3.0-dev.1",
46
+ "@chitchat/sdk-protocol": "0.3.0-dev.1",
47
+ "@chitchat/sdk-web": "0.3.0-dev.1"
48
+ },
49
+ "keywords": [
50
+ "chitchat",
51
+ "realtime",
52
+ "react-native",
53
+ "tls",
54
+ "protobuf"
55
+ ],
16
56
  "sideEffects": false,
17
57
  "license": "UNLICENSED",
18
- "publishConfig": { "access": "public", "tag": "development" }
58
+ "publishConfig": {
59
+ "access": "public",
60
+ "tag": "development"
61
+ },
62
+ "peerDependencies": {
63
+ "react-native": ">=0.76 <1",
64
+ "react-native-webrtc": ">=124 <125",
65
+ "react": ">=18 <20"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "react-native-webrtc": {
69
+ "optional": true
70
+ }
71
+ }
19
72
  }
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ sourceDir: './android',
6
+ packageImportPath: 'import com.chitchat.sdk.ChitChatTcpPackage;',
7
+ packageInstance: 'new ChitChatTcpPackage()'
8
+ },
9
+ ios: { podspecPath: './ChitChatTcp.podspec' }
10
+ }
11
+ }
12
+ };
package/react.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { ReactElement } from 'react';
2
+ import type { NativeCallsClient, NativeMediaStream } from './index';
3
+ import type { CallSnapshot } from '@chitchat/sdk-core';
4
+ export function useCallState(client: NativeCallsClient): CallSnapshot<NativeMediaStream>;
5
+ export function CallView(props: { stream: NativeMediaStream | null; mirror?: boolean; objectFit?: 'contain' | 'cover'; zOrder?: number; style?: unknown }): ReactElement | null;
package/src/index.js CHANGED
@@ -1,7 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  let ReliableRealtimeClient;
4
- try { ({ ReliableRealtimeClient } = require('@chitchat/sdk-core')); } catch (_) { ({ ReliableRealtimeClient } = require('../../chitchat-core/src')); }
4
+ try {
5
+ ({ ReliableRealtimeClient } = require('@chitchat/sdk-core'));
6
+ } catch (_) {
7
+ // Monorepo dev fallback — constructed dynamically so Metro never statically
8
+ // bundles a dead relative path when the scoped package is present.
9
+ const monorepoCore = ['..', '..', 'chitchat-core', 'src'].join('/');
10
+ ({ ReliableRealtimeClient } = require(monorepoCore));
11
+ }
5
12
 
6
13
  const validateNativeTlsEndpoint = (endpoint) => {
7
14
  if (typeof endpoint !== 'string' || !endpoint.trim() || /\s/.test(endpoint)) throw new Error('A TLS host endpoint is required');
@@ -32,23 +39,45 @@ const createNativeTcpTransportFactory = ({ nativeModule, pinnedPublicKeyHashes,
32
39
  const pins = validatePinnedPublicKeyHashes(pinnedPublicKeyHashes);
33
40
  return ({ endpoint, token, protocol }) => {
34
41
  const normalizedEndpoint = validateNativeTlsEndpoint(endpoint);
35
- let connection; let onMessage = () => {}; let onClose = () => {};
42
+ const abort = new AbortController();
43
+ let connection; let cancelled = false; let onMessage = () => {}; let onClose = () => {};
36
44
  return Promise.resolve({
37
45
  onMessage: (listener) => { onMessage = listener; },
38
46
  onClose: (listener) => { onClose = listener; },
39
- open: () => Promise.resolve(nativeModule.connect({ endpoint: normalizedEndpoint, token, protocol, tls: true, minimumTlsVersion, pinnedPublicKeyHashes: pins }))
47
+ open: () => Promise.resolve(nativeModule.connect({ endpoint: normalizedEndpoint, token, protocol, signal: abort.signal, tls: true, minimumTlsVersion, pinnedPublicKeyHashes: pins }))
40
48
  .then(assertConnection)
41
49
  .then((openedConnection) => {
50
+ if (cancelled) { void Promise.resolve(openedConnection.close()).catch(() => {}); throw new Error('Native connection was cancelled'); }
42
51
  connection = openedConnection;
43
52
  connection.onFrame((frame) => onMessage(frame));
44
53
  connection.onClose((reason) => onClose(reason));
45
54
  }),
46
55
  send: (frame) => connection ? Promise.resolve(connection.send(frame)) : Promise.reject(new Error('Native TCP transport is not open')),
47
- close: () => connection ? Promise.resolve(connection.close()) : Promise.resolve()
56
+ close: () => { cancelled = true; abort.abort(); return connection ? Promise.resolve(connection.close()) : Promise.resolve(); }
48
57
  });
49
58
  };
50
59
  };
51
60
 
52
- const createChitChatNativeClient = ({ endpoint, tokenProvider, nativeModule, pinnedPublicKeyHashes, minimumTlsVersion, ...options }) => new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'native', nativeTransportFactory: createNativeTcpTransportFactory({ nativeModule, pinnedPublicKeyHashes, minimumTlsVersion }), ...options });
61
+ const createChitChatNativeClient = ({ endpoint, tokenProvider, nativeModule, protobuf, deviceId, idFactory, pinnedPublicKeyHashes, minimumTlsVersion, ...options }) => {
62
+ let bridge = nativeModule;
63
+ if (!bridge) {
64
+ const { NativeModules, NativeEventEmitter, Platform } = require('react-native');
65
+ const module = NativeModules.ChitChatTcp;
66
+ if (!module) throw new Error('ChitChatTcp is not linked. Install pods and rebuild the native app; Expo Go is unsupported.');
67
+ idFactory = idFactory || (typeof module.randomUUID === 'function' ? () => module.randomUUID() : undefined);
68
+ let codecs;
69
+ try { codecs = require('@chitchat/sdk-protocol'); } catch (_) { codecs = require(monorepoPath('protocol')); }
70
+ bridge = require('./protobuf-bridge').createChitChatNativeBridge({ nativeModule: module, eventEmitter: new NativeEventEmitter(module), protobuf: protobuf || codecs.createRealtimeEnvelopeCodec({ deviceId, platform: Platform.OS, idFactory }) });
71
+ }
72
+ return new ReliableRealtimeClient({ endpoint, tokenProvider, runtime: 'native', idFactory, nativeTransportFactory: createNativeTcpTransportFactory({ nativeModule: bridge, pinnedPublicKeyHashes, minimumTlsVersion }), ...options });
73
+ };
74
+
75
+ const monorepoPath = (name) => ['..', '..', `chitchat-${name}`, 'src'].join('/');
76
+ // Dynamic monorepo fallback so Metro never statically bundles dead relative paths.
77
+ const core = () => { try { return require('@chitchat/sdk-core'); } catch (_) { return require(monorepoPath('core')); } };
78
+ const web = () => { try { return require('@chitchat/sdk-web'); } catch (_) { return require(monorepoPath('web')); } };
79
+ // React Native ships WebSocket; using WSS removes the private TCP bridge requirement.
80
+ const createChitChatNativeWssClient = (options) => web().createChitChatWebClient(options);
81
+ const createChitChatNativeCalls = ({ rtc, ...options } = {}) => new (core().CallsClient)({ ...options, rtc: rtc || require('react-native-webrtc') });
53
82
 
54
- module.exports = { createChitChatNativeClient, createNativeTcpTransportFactory, validateNativeTlsEndpoint, validatePinnedPublicKeyHashes };
83
+ module.exports = { ...require('./protobuf-bridge'), createChitChatNativeWssClient, createChitChatNativeCalls, createChitChatNativeClient, createNativeTcpTransportFactory, validateNativeTlsEndpoint, validatePinnedPublicKeyHashes };
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ // Adapts the existing iOS/Android TLS byte-stream module. The public codec,
4
+ // handshake and frame limits live in JS; native code never sees a token string.
5
+ let protocol;
6
+ try { protocol = require('@chitchat/sdk-protocol'); } catch (_) { protocol = require(['..', '..', 'chitchat-protocol', 'src'].join('/')); }
7
+ const leased = new WeakSet();
8
+ const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
9
+ const toBase64 = (bytes) => {
10
+ let result = '';
11
+ for (let i = 0; i < bytes.length; i += 3) {
12
+ const n = (bytes[i] << 16) | ((bytes[i + 1] || 0) << 8) | (bytes[i + 2] || 0);
13
+ result += alphabet[(n >>> 18) & 63] + alphabet[(n >>> 12) & 63] + (i + 1 < bytes.length ? alphabet[(n >>> 6) & 63] : '=') + (i + 2 < bytes.length ? alphabet[n & 63] : '=');
14
+ }
15
+ return result;
16
+ };
17
+ const fromBase64 = (text) => {
18
+ if (typeof text !== 'string' || text.length > 1_398_108 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(text)) throw new Error('Invalid native binary frame');
19
+ const result = new Uint8Array(text.length / 4 * 3 - (text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0));
20
+ let offset = 0;
21
+ for (let i = 0; i < text.length; i += 4) {
22
+ const n = (alphabet.indexOf(text[i]) << 18) | (alphabet.indexOf(text[i + 1]) << 12) | (Math.max(0, alphabet.indexOf(text[i + 2])) << 6) | Math.max(0, alphabet.indexOf(text[i + 3]));
23
+ result[offset++] = (n >>> 16) & 255; if (offset < result.length) result[offset++] = (n >>> 8) & 255; if (offset < result.length) result[offset++] = n & 255;
24
+ }
25
+ return result;
26
+ };
27
+
28
+ function createChitChatNativeBridge({ nativeModule, eventEmitter, protobuf, openTimeoutMs = 15_000 } = {}) {
29
+ if (!nativeModule?.supportsTLS || !nativeModule.supportsProtobufFrames || ['connectProtobuf', 'sendProtobufFrame', 'disconnect'].some((key) => typeof nativeModule[key] !== 'function')) throw new Error('A TLS-capable ChitChat protobuf native module is required');
30
+ if (typeof eventEmitter?.addListener !== 'function' || !protobuf?.createTcpDecoder || !protobuf.encodeAuth || !protobuf.isAuthenticationAccepted) throw new Error('Native event emitter and official protobuf codec are required');
31
+ if (!Number.isInteger(openTimeoutMs) || openTimeoutMs < 100 || openTimeoutMs > 120_000) throw new Error('openTimeoutMs must be between 100 and 120000');
32
+ return {
33
+ connect({ endpoint, token, protocol: subprotocol, tls, minimumTlsVersion, pinnedPublicKeyHashes = [], signal }) {
34
+ if (tls !== true || subprotocol !== 'chitchat.protobuf.v1') return Promise.reject(new Error('TLS protobuf is required'));
35
+ // Existing app modules enforce system trust and TLS >= 1.2. They do not
36
+ // implement pinning or a TLS-1.3-only option; never silently ignore either.
37
+ if (pinnedPublicKeyHashes.length || minimumTlsVersion === 'TLSv1.3') return Promise.reject(new Error('The linked native bridge does not support the requested TLS policy'));
38
+ if (leased.has(nativeModule)) return Promise.reject(new Error('Native module already has an active SDK connection'));
39
+ const url = new URL(`tls://${endpoint}`);
40
+ leased.add(nativeModule);
41
+ return new Promise((resolve, reject) => {
42
+ const decoder = protobuf.createTcpDecoder(); const subscriptions = [];
43
+ let ready = false; let closed = false; let timer; let messageListener = () => {}; let closeListener = () => {}; let authSent = false;
44
+ const close = (code = 'NATIVE_CONNECTION_CLOSED') => {
45
+ if (closed) return; closed = true; clearTimeout(timer); subscriptions.forEach((subscription) => subscription.remove()); decoder.reset(); leased.delete(nativeModule);
46
+ try { Promise.resolve(nativeModule.disconnect()).catch(() => {}); } catch (_) {}
47
+ reject(new Error(code)); closeListener({ code });
48
+ };
49
+ const sendBytes = (bytes) => Promise.resolve().then(() => { if (closed) throw new Error('Native transport is closed'); return nativeModule.sendProtobufFrame(toBase64(bytes)); });
50
+ const connection = {
51
+ onFrame: (listener) => { messageListener = listener; }, onClose: (listener) => { closeListener = listener; },
52
+ send: (frame) => ready && !closed ? sendBytes(protobuf.encodeTcpFrame(frame)) : Promise.reject(new Error('Native handshake is incomplete')),
53
+ close: async () => close()
54
+ };
55
+ const abort = () => close('NATIVE_CONNECTION_CANCELLED');
56
+ if (signal?.aborted) { close('NATIVE_CONNECTION_CANCELLED'); return; }
57
+ signal?.addEventListener('abort', abort, { once: true });
58
+ subscriptions.push({ remove: () => signal?.removeEventListener('abort', abort) });
59
+ try {
60
+ subscriptions.push(eventEmitter.addListener('onTransportOpen', () => {
61
+ if (closed || authSent) return; authSent = true;
62
+ void sendBytes(protocol.encodeTcpFrame(protobuf.encodeAuth({ token, protocol: subprotocol }))).catch(() => close('NATIVE_AUTH_SEND_FAILED'));
63
+ }));
64
+ subscriptions.push(eventEmitter.addListener('onProtobufFrame', (data) => {
65
+ if (closed) return;
66
+ try {
67
+ for (const frame of decoder.push(fromBase64(data?.frame))) {
68
+ if (!ready) {
69
+ if (!protobuf.isAuthenticationAccepted(frame)) { close('NATIVE_AUTHENTICATION_REJECTED'); return; }
70
+ ready = true; clearTimeout(timer); resolve(connection);
71
+ continue;
72
+ }
73
+ // Defer a same-chunk post-auth frame until the adapter attaches.
74
+ Promise.resolve().then(() => Promise.resolve()).then(() => { if (!closed) messageListener(frame); });
75
+ }
76
+ } catch (_) { close('PROTOCOL_DECODE_ERROR'); }
77
+ }));
78
+ subscriptions.push(eventEmitter.addListener('onError', () => close('NATIVE_TRANSPORT_ERROR')));
79
+ subscriptions.push(eventEmitter.addListener('onDisconnected', () => close()));
80
+ timer = setTimeout(() => close('NATIVE_CONNECTION_TIMEOUT'), openTimeoutMs);
81
+ Promise.resolve(nativeModule.connectProtobuf(url.hostname.replace(/^\[|\]$/g, ''), Number(url.port || 443), true)).catch(() => close('NATIVE_CONNECTION_FAILED'));
82
+ } catch (_) { close('NATIVE_CONNECTION_FAILED'); }
83
+ });
84
+ }
85
+ };
86
+ }
87
+ module.exports = { createChitChatNativeBridge };
package/src/react.js ADDED
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+ const React = require('react');
3
+ function useCallState(client) {
4
+ const subscribe = React.useCallback((changed) => client.on('stateChange', changed), [client]);
5
+ const snapshot = React.useCallback(() => client.getSnapshot(), [client]);
6
+ return React.useSyncExternalStore(subscribe, snapshot, snapshot);
7
+ }
8
+ function CallView({ stream, ...props }) {
9
+ const { RTCView } = require('react-native-webrtc');
10
+ return stream ? React.createElement(RTCView, { ...props, streamURL: stream.toURL() }) : null;
11
+ }
12
+ module.exports = { useCallState, CallView };