react-native-newinstance-chat 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lawrence
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,23 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "NewinstanceChat"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/talktothelaw/react-native-newinstance-chat.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ # Native chat SDK we wrap, published on CocoaPods Trunk.
20
+ s.dependency "LiveAndAiChat", "~> 0.1"
21
+
22
+ install_modules_dependencies(s)
23
+ end
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # react-native-newinstance-chat
2
+
3
+ React Native bindings for [LiveAndAiChat](https://newinstance.cloud) — drops a
4
+ complete AI + live-agent chat experience into your iOS and Android RN app
5
+ via native module bridges.
6
+
7
+ The chat UI, transport, attachments, image cache, typing indicators, and
8
+ silent gap-fill resync all come from the native iOS / Android SDKs. This
9
+ package is a thin TurboModule surface over them — your React Native code
10
+ stays small.
11
+
12
+ ## Requirements
13
+
14
+ - React Native 0.74+ (TurboModule support — recommended 0.85+)
15
+ - iOS 15.1+
16
+ - Android API 24+, JDK 17
17
+
18
+ ## Installation
19
+
20
+ ```sh
21
+ npm install react-native-newinstance-chat
22
+ # or
23
+ yarn add react-native-newinstance-chat
24
+ ```
25
+
26
+ ### iOS
27
+
28
+ ```sh
29
+ cd ios && pod install
30
+ ```
31
+
32
+ Autolink pulls the `NewinstanceChat` podspec, which transitively depends
33
+ on `LiveAndAiChat` (the native SDK pod, published on CocoaPods Trunk).
34
+
35
+ ### Android
36
+
37
+ Autolink registers `NewinstanceChatPackage`. The underlying
38
+ `cloud.newinstance:liveandaichat` AAR is resolved from Maven Central —
39
+ ensure `mavenCentral()` is in `settings.gradle.kts`.
40
+
41
+ ## Quick start
42
+
43
+ ```tsx
44
+ import { NewinstanceChat } from 'react-native-newinstance-chat';
45
+
46
+ const sdk = new NewinstanceChat({
47
+ apiKey: 'sk_live_…',
48
+ });
49
+
50
+ sdk.setUser({
51
+ customerName: 'Ada Lovelace',
52
+ customerEmail: 'ada@example.com',
53
+ });
54
+
55
+ await sdk.initialize();
56
+
57
+ // Later, in response to a host button tap:
58
+ sdk.openChat();
59
+ ```
60
+
61
+ The chat screen renders natively — there is no React component to mount.
62
+ Your RN UI is whatever button / banner / FAB triggers `openChat()`.
63
+
64
+ ### Subscribing to events
65
+
66
+ ```ts
67
+ const unsub = sdk.addListener('messageReceived', (m) => {
68
+ console.log('rx <-', m.content);
69
+ });
70
+ // Later:
71
+ unsub();
72
+ ```
73
+
74
+ Event names: `messageReceived`, `messageSent`, `agentTypingChanged`,
75
+ `connectionStateChanged`, `unreadCountChanged`, `error`. See `src/types.ts`
76
+ for payload shapes.
77
+
78
+ ### Sending messages programmatically
79
+
80
+ ```ts
81
+ sdk.sendMessage('Hi, I have a question.');
82
+ sdk.attachFile({ uri: 'file:///…', name: 'screenshot.png', mimeType: 'image/png' });
83
+ sdk.retryMessage(failedMessageId);
84
+ ```
85
+
86
+ Handoff to a live agent and typing indicators are driven automatically
87
+ by the native chat screen — there's nothing to wire up from the host.
88
+
89
+ ### Tear-down
90
+
91
+ ```ts
92
+ sdk.destroy();
93
+ ```
94
+
95
+ Call this when your host component unmounts. After `destroy()` the
96
+ instance is unusable — construct a new one if you need to chat again.
97
+
98
+ ## Contributing
99
+
100
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
101
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
102
+ - [Code of conduct](CODE_OF_CONDUCT.md)
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,79 @@
1
+ buildscript {
2
+ ext.NewinstanceChat = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return NewinstanceChat[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.newinstancechat"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ repositories {
66
+ google()
67
+ mavenCentral()
68
+ // Dev fallback: pick up `cloud.newinstance:liveandaichat:0.1.0` from
69
+ // ~/.m2/repository if the developer ran `./gradlew :sdk:publishToMavenLocal`
70
+ // in liveAndAiChat-android/ but the Maven Central publish hasn't
71
+ // landed yet. Remove this once the SDK is live on Maven Central.
72
+ mavenLocal()
73
+ }
74
+
75
+ dependencies {
76
+ implementation "com.facebook.react:react-android"
77
+ // Native chat SDK the bridge wraps. Published under cloud.newinstance.
78
+ implementation "cloud.newinstance:liveandaichat:0.1.0"
79
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,200 @@
1
+ package com.newinstancechat
2
+
3
+ import cloud.newinstance.liveandaichat.ConnectionState
4
+ import cloud.newinstance.liveandaichat.LiveAndAiChat
5
+ import cloud.newinstance.liveandaichat.LiveAndAiChatConfig
6
+ import cloud.newinstance.liveandaichat.LiveAndAiChatError
7
+ import cloud.newinstance.liveandaichat.ChatUser
8
+ import cloud.newinstance.liveandaichat.model.ChatMessage
9
+ import cloud.newinstance.liveandaichat.model.TransportMode
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.facebook.react.bridge.Promise
12
+ import com.facebook.react.bridge.ReactApplicationContext
13
+ import com.facebook.react.bridge.ReadableMap
14
+ import com.facebook.react.bridge.WritableMap
15
+ import com.facebook.react.modules.core.DeviceEventManagerModule
16
+
17
+ /**
18
+ * React Native ↔ LiveAndAiChat Android SDK bridge (TurboModule).
19
+ *
20
+ * Holds a single underlying `LiveAndAiChat` instance built lazily on
21
+ * `configure(...)`. Repeated `configure(...)` calls tear down and
22
+ * rebuild — hosts almost never want concurrent chat sessions.
23
+ *
24
+ * Events emitted to JS via `RCTDeviceEventEmitter`:
25
+ * - `messageReceived` (ChatMessage map)
26
+ * - `messageSent` (ChatMessage map)
27
+ * - `agentTypingChanged` (Boolean)
28
+ * - `connectionStateChanged` (lowercased connection state name)
29
+ * - `unreadCountChanged` (Int)
30
+ * - `error` ({type, message, recoverable})
31
+ */
32
+ class NewinstanceChatModule(reactContext: ReactApplicationContext) :
33
+ NativeNewinstanceChatSpec(reactContext), LiveAndAiChat.Listener {
34
+
35
+ private var sdk: LiveAndAiChat? = null
36
+ private var pendingUser: ChatUser? = null
37
+
38
+ override fun getName(): String = NAME
39
+
40
+ private fun emit(event: String, body: Any?) {
41
+ reactApplicationContext
42
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
43
+ .emit(event, body)
44
+ }
45
+
46
+ // MARK: - configure / setUser / initialize
47
+
48
+ override fun configure(config: ReadableMap, promise: Promise) {
49
+ val apiKey = config.getString("apiKey")
50
+ if (apiKey.isNullOrBlank()) {
51
+ promise.reject("E_CONFIG", "apiKey is required")
52
+ return
53
+ }
54
+ val baseUrl = if (config.hasKey("baseUrl")) config.getString("baseUrl") else null
55
+ val initialMessage =
56
+ if (config.hasKey("initialMessage")) config.getString("initialMessage") else null
57
+ val transport: TransportMode? = if (config.hasKey("transport")) {
58
+ when (config.getString("transport")) {
59
+ "sse" -> TransportMode.SSE
60
+ "ws" -> TransportMode.WS
61
+ else -> null
62
+ }
63
+ } else null
64
+
65
+ try {
66
+ val cfg = LiveAndAiChatConfig(
67
+ apiKey = apiKey,
68
+ baseUrl = baseUrl ?: LiveAndAiChatConfig.DEFAULT_BASE_URL,
69
+ transport = transport,
70
+ initialMessage = initialMessage,
71
+ )
72
+ sdk?.destroy()
73
+ val builder = LiveAndAiChat.Builder(reactApplicationContext).config(cfg)
74
+ pendingUser?.let { builder.user(it) }
75
+ val instance = builder.build()
76
+ instance.addListener(this)
77
+ sdk = instance
78
+ pendingUser = null
79
+ promise.resolve(null)
80
+ } catch (e: Exception) {
81
+ promise.reject("E_BUILD", e.message, e)
82
+ }
83
+ }
84
+
85
+ override fun setUser(user: ReadableMap) {
86
+ val name = user.getString("customerName") ?: return
87
+ if (name.isBlank()) return
88
+ val chatUser = ChatUser(
89
+ customerName = name,
90
+ customerId = if (user.hasKey("customerId")) user.getString("customerId") else null,
91
+ customerEmail = if (user.hasKey("customerEmail")) user.getString("customerEmail") else null,
92
+ )
93
+ val current = sdk
94
+ if (current != null) {
95
+ current.setUser(chatUser)
96
+ } else {
97
+ pendingUser = chatUser
98
+ }
99
+ }
100
+
101
+ override fun initialize(promise: Promise) {
102
+ val current = sdk
103
+ if (current == null) {
104
+ promise.reject("E_NOT_CONFIGURED", "configure() must be called before initialize()")
105
+ return
106
+ }
107
+ current.initialize()
108
+ promise.resolve(null)
109
+ }
110
+
111
+ // MARK: - Native chat presentation
112
+
113
+ override fun openChat() { sdk?.openChat() }
114
+ override fun closeChat() { sdk?.closeChat() }
115
+
116
+ // MARK: - Programmatic actions
117
+
118
+ override fun sendMessage(text: String) { sdk?.sendMessage(text) }
119
+ override fun retryMessage(messageId: String) { sdk?.retryMessage(messageId) }
120
+
121
+ override fun attachFile(opts: ReadableMap, promise: Promise) {
122
+ val current = sdk
123
+ if (current == null) {
124
+ promise.reject("E_NOT_CONFIGURED", "configure() must be called first")
125
+ return
126
+ }
127
+ val uri = opts.getString("uri")
128
+ val name = opts.getString("name")
129
+ val mimeType = opts.getString("mimeType")
130
+ if (uri == null || name == null || mimeType == null) {
131
+ promise.reject("E_ARGS", "uri, name, mimeType are required")
132
+ return
133
+ }
134
+ try {
135
+ val bytes = reactApplicationContext.contentResolver
136
+ .openInputStream(android.net.Uri.parse(uri))?.use { it.readBytes() }
137
+ ?: throw java.io.IOException("Could not open content URI: $uri")
138
+ current.attachFile(bytes, name, mimeType, uri)
139
+ promise.resolve(null)
140
+ } catch (e: Exception) {
141
+ promise.reject("E_READ", e.message, e)
142
+ }
143
+ }
144
+
145
+ override fun destroy() {
146
+ sdk?.destroy()
147
+ sdk = null
148
+ pendingUser = null
149
+ }
150
+
151
+ // RN NativeEventEmitter listener tracking — required by Codegen even
152
+ // though the actual emit path is RCTDeviceEventEmitter (above).
153
+ override fun addListener(eventName: String) { /* no-op */ }
154
+ override fun removeListeners(count: Double) { /* no-op */ }
155
+
156
+ // MARK: - LiveAndAiChat.Listener
157
+
158
+ override fun onMessageReceived(message: ChatMessage) {
159
+ emit("messageReceived", chatMessageToMap(message))
160
+ }
161
+ override fun onMessageSent(message: ChatMessage) {
162
+ emit("messageSent", chatMessageToMap(message))
163
+ }
164
+ override fun onTyping(isTyping: Boolean) {
165
+ emit("agentTypingChanged", isTyping)
166
+ }
167
+ override fun onConnectionChange(state: ConnectionState) {
168
+ emit("connectionStateChanged", state.name.lowercase())
169
+ }
170
+ override fun onUnreadCountChange(count: Int) {
171
+ emit("unreadCountChanged", count)
172
+ }
173
+ override fun onError(error: LiveAndAiChatError) {
174
+ val map = Arguments.createMap()
175
+ map.putString("type", error.type.name.lowercase())
176
+ map.putString("message", error.message)
177
+ map.putBoolean("recoverable", error.recoverable)
178
+ emit("error", map)
179
+ }
180
+
181
+ // MARK: - Helpers
182
+
183
+ private fun chatMessageToMap(m: ChatMessage): WritableMap {
184
+ val map = Arguments.createMap()
185
+ map.putString("id", m.id)
186
+ map.putString("clientId", m.clientId)
187
+ map.putString("conversationId", m.conversationId)
188
+ map.putString("content", m.content)
189
+ map.putString("type", m.type.name.lowercase())
190
+ map.putString("status", m.status.name.lowercase())
191
+ m.seq?.let { map.putDouble("seq", it.toDouble()) }
192
+ m.sentAt?.let { map.putString("sentAt", it) }
193
+ m.totalMessages?.let { map.putInt("totalMessages", it) }
194
+ return map
195
+ }
196
+
197
+ companion object {
198
+ const val NAME = NativeNewinstanceChatSpec.NAME
199
+ }
200
+ }
@@ -0,0 +1,31 @@
1
+ package com.newinstancechat
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class NewinstanceChatPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == NewinstanceChatModule.NAME) {
13
+ NewinstanceChatModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ NewinstanceChatModule.NAME to ReactModuleInfo(
22
+ name = NewinstanceChatModule.NAME,
23
+ className = NewinstanceChatModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = true
28
+ )
29
+ )
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ #import <NewinstanceChatSpec/NewinstanceChatSpec.h>
2
+
3
+ @class NewinstanceChatImpl;
4
+
5
+ @interface NewinstanceChat : NSObject <NativeNewinstanceChatSpec>
6
+
7
+ /// Emit a JS-side `RCTDeviceEventEmitter` event. Called by
8
+ /// NewinstanceChatImpl (Swift) when the underlying LiveAndAiChat SDK
9
+ /// surfaces a delegate callback.
10
+ - (void)emitEvent:(NSString *)name body:(id)body;
11
+
12
+ @end
@@ -0,0 +1,103 @@
1
+ #import "NewinstanceChat.h"
2
+ #import <React/RCTBridge.h>
3
+ #import <React/RCTEventDispatcherProtocol.h>
4
+
5
+ // Auto-generated Swift bridging header for this pod (created at build
6
+ // time by Xcode/CocoaPods when the pod mixes Obj-C and Swift). The pod
7
+ // name is `NewinstanceChat`, hence the header name.
8
+ #import "NewinstanceChat-Swift.h"
9
+
10
+ // Declare conformance to NewinstanceChatEmitter (defined in
11
+ // NewinstanceChatImpl.swift) so the Swift impl can hold a typed weak
12
+ // reference to us. The auto-generated NewinstanceChat-Swift.h imported
13
+ // above provides the protocol declaration.
14
+ @interface NewinstanceChat () <NewinstanceChatEmitter>
15
+ @property (nonatomic, strong) NewinstanceChatImpl *impl;
16
+ @end
17
+
18
+ @implementation NewinstanceChat
19
+
20
+ @synthesize bridge = _bridge;
21
+
22
+ RCT_EXPORT_MODULE()
23
+
24
+ - (instancetype)init
25
+ {
26
+ if ((self = [super init])) {
27
+ _impl = [[NewinstanceChatImpl alloc] init];
28
+ _impl.emitter = self;
29
+ }
30
+ return self;
31
+ }
32
+
33
+ + (BOOL)requiresMainQueueSetup
34
+ {
35
+ return YES;
36
+ }
37
+
38
+ // MARK: - Spec methods (delegated to Swift)
39
+
40
+ - (void)configure:(NSDictionary *)config
41
+ resolve:(RCTPromiseResolveBlock)resolve
42
+ reject:(RCTPromiseRejectBlock)reject
43
+ {
44
+ [_impl configure:config resolver:resolve rejecter:reject];
45
+ }
46
+
47
+ - (void)setUser:(NSDictionary *)user
48
+ {
49
+ [_impl setUser:user];
50
+ }
51
+
52
+ - (void)initialize:(RCTPromiseResolveBlock)resolve
53
+ reject:(RCTPromiseRejectBlock)reject
54
+ {
55
+ [_impl initializeWithResolver:resolve rejecter:reject];
56
+ }
57
+
58
+ - (void)openChat { [_impl openChat]; }
59
+ - (void)closeChat { [_impl closeChat]; }
60
+ - (void)destroy { [_impl destroy]; }
61
+
62
+ - (void)sendMessage:(NSString *)text { [_impl sendMessage:text]; }
63
+ - (void)retryMessage:(NSString *)messageId { [_impl retryMessage:messageId]; }
64
+
65
+ - (void)attachFile:(NSDictionary *)opts
66
+ resolve:(RCTPromiseResolveBlock)resolve
67
+ reject:(RCTPromiseRejectBlock)reject
68
+ {
69
+ [_impl attachFile:opts resolver:resolve rejecter:reject];
70
+ }
71
+
72
+ // NativeEventEmitter wires these so it can track subscriber counts. The
73
+ // actual event payloads flow through RCTDeviceEventEmitter, so these are
74
+ // intentional no-ops.
75
+ - (void)addListener:(NSString *)eventName { }
76
+ - (void)removeListeners:(double)count { }
77
+
78
+ // MARK: - Event emission helper (called from Swift)
79
+
80
+ - (void)emitEvent:(NSString *)name body:(id)body
81
+ {
82
+ // RCTDeviceEventEmitter is registered on every RN bridge instance.
83
+ // `enqueueJSCall:method:args:completion:` works in both bridge mode
84
+ // and bridgeless mode (the new arch shims it). The `bridge` property
85
+ // is synthesised by RCT_EXPORT_MODULE; nil in unusual bridgeless
86
+ // setups where the module is constructed without an attached bridge.
87
+ if (!self.bridge) return;
88
+ id payload = body ?: [NSNull null];
89
+ [self.bridge enqueueJSCall:@"RCTDeviceEventEmitter"
90
+ method:@"emit"
91
+ args:@[name, payload]
92
+ completion:NULL];
93
+ }
94
+
95
+ // MARK: - TurboModule registration
96
+
97
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
98
+ (const facebook::react::ObjCTurboModule::InitParams &)params
99
+ {
100
+ return std::make_shared<facebook::react::NativeNewinstanceChatSpecJSI>(params);
101
+ }
102
+
103
+ @end