@zellio/chat-react-native 0.0.0-stage → 1.0.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 +202 -0
- package/README.md +239 -2
- package/ZellioChatReactNative.podspec +30 -0
- package/android/build.gradle +62 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatModule.kt +214 -0
- package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatPackage.kt +25 -0
- package/app.plugin.js +4 -0
- package/ios/RNZellioChat.h +5 -0
- package/ios/RNZellioChat.mm +116 -0
- package/ios/ZCRNBridge.swift +217 -0
- package/ios/vendor/resources/PrivacyInfo.xcprivacy +99 -0
- package/ios/vendor/swift/ZellioChat/Model/ChatModel.swift +261 -0
- package/ios/vendor/swift/ZellioChat/Platform/Platform.swift +124 -0
- package/ios/vendor/swift/ZellioChat/Platform/URLSessionTransport.swift +175 -0
- package/ios/vendor/swift/ZellioChat/Preview/Previews.swift +62 -0
- package/ios/vendor/swift/ZellioChat/Public/InAppBanner.swift +155 -0
- package/ios/vendor/swift/ZellioChat/Public/PublicTypes.swift +141 -0
- package/ios/vendor/swift/ZellioChat/Public/Runtime.swift +406 -0
- package/ios/vendor/swift/ZellioChat/Public/ZellioChat.swift +209 -0
- package/ios/vendor/swift/ZellioChat/Theme/ChatTheme.swift +185 -0
- package/ios/vendor/swift/ZellioChat/Thread/AttachmentPicker.swift +354 -0
- package/ios/vendor/swift/ZellioChat/Thread/ComposerBar.swift +355 -0
- package/ios/vendor/swift/ZellioChat/Thread/ThreadCells.swift +776 -0
- package/ios/vendor/swift/ZellioChat/Thread/ThreadViewController.swift +703 -0
- package/ios/vendor/swift/ZellioChat/Views/ChatRootView.swift +154 -0
- package/ios/vendor/swift/ZellioChat/Views/Components.swift +335 -0
- package/ios/vendor/swift/ZellioChat/Views/HomeView.swift +413 -0
- package/ios/vendor/swift/ZellioChatCore/ChatModels.swift +373 -0
- package/ios/vendor/swift/ZellioChatCore/ConfigModels.swift +389 -0
- package/ios/vendor/swift/ZellioChatCore/Engine.swift +325 -0
- package/ios/vendor/swift/ZellioChatCore/Errors.swift +158 -0
- package/ios/vendor/swift/ZellioChatCore/Generated/ChatStringTable.swift +125 -0
- package/ios/vendor/swift/ZellioChatCore/Outbox.swift +223 -0
- package/ios/vendor/swift/ZellioChatCore/Push.swift +74 -0
- package/ios/vendor/swift/ZellioChatCore/Realtime.swift +328 -0
- package/ios/vendor/swift/ZellioChatCore/Session.swift +210 -0
- package/ios/vendor/swift/ZellioChatCore/Storage.swift +201 -0
- package/ios/vendor/swift/ZellioChatCore/Strings.swift +104 -0
- package/ios/vendor/swift/ZellioChatCore/Theme.swift +238 -0
- package/ios/vendor/swift/ZellioChatCore/ThreadController.swift +319 -0
- package/ios/vendor/swift/ZellioChatCore/Timeline.swift +157 -0
- package/ios/vendor/swift/ZellioChatCore/Transport.swift +236 -0
- package/ios/vendor/swift/ZellioChatCore/Util.swift +137 -0
- package/lib/NativeZellioChat.d.ts +34 -0
- package/lib/NativeZellioChat.js +4 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +241 -0
- package/lib/types.d.ts +54 -0
- package/lib/types.js +2 -0
- package/lib/version.d.ts +4 -0
- package/lib/version.js +7 -0
- package/package.json +75 -4
- package/plugin/build/index.js +73 -0
- package/src/NativeZellioChat.ts +30 -0
- package/src/index.ts +237 -0
- package/src/types.ts +56 -0
- package/src/version.ts +5 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
package ai.zellio.chat.reactnative
|
|
2
|
+
|
|
3
|
+
import ai.zellio.chat.ZellioChat
|
|
4
|
+
import ai.zellio.chat.ZellioChatListener
|
|
5
|
+
import ai.zellio.chat.ZellioChatOptions
|
|
6
|
+
import ai.zellio.chat.ZellioContext
|
|
7
|
+
import ai.zellio.chat.ZellioLocale
|
|
8
|
+
import ai.zellio.chat.ZellioLogLevel
|
|
9
|
+
import ai.zellio.chat.ZellioTheme
|
|
10
|
+
import ai.zellio.chat.ZellioThemeMode
|
|
11
|
+
import ai.zellio.chat.ZellioTokenCallbackProvider
|
|
12
|
+
import ai.zellio.chat.ZellioUnreadListener
|
|
13
|
+
import android.os.Handler
|
|
14
|
+
import android.os.Looper
|
|
15
|
+
import android.util.Log
|
|
16
|
+
import com.facebook.react.bridge.Arguments
|
|
17
|
+
import com.facebook.react.bridge.Promise
|
|
18
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
19
|
+
import com.facebook.react.bridge.ReadableMap
|
|
20
|
+
import java.util.UUID
|
|
21
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The Turbo Native Module. It holds no chat logic: every method calls the ZeeChat Android SDK and returns.
|
|
25
|
+
* No exception leaves a method; the SDK has the same rule.
|
|
26
|
+
*/
|
|
27
|
+
class ZellioChatModule(reactContext: ReactApplicationContext) : NativeZellioChatSpec(reactContext) {
|
|
28
|
+
private val main = Handler(Looper.getMainLooper())
|
|
29
|
+
private val pendingTokens = ConcurrentHashMap<String, ZellioTokenCallbackProvider.Callback>()
|
|
30
|
+
|
|
31
|
+
private val chatListener = object : ZellioChatListener {
|
|
32
|
+
override fun onChatOpened() = emitEvent("chatOpened", null)
|
|
33
|
+
override fun onChatClosed() = emitEvent("chatClosed", null)
|
|
34
|
+
override fun onMessageSent() = emitEvent("messageSent", null)
|
|
35
|
+
override fun onMessageReceived() = emitEvent("messageReceived", null)
|
|
36
|
+
override fun onError(code: String) = emitEvent("error", code)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private val unreadListener = ZellioUnreadListener { count ->
|
|
40
|
+
guarded("unread") {
|
|
41
|
+
val payload = Arguments.createMap()
|
|
42
|
+
payload.putDouble("count", count.toDouble())
|
|
43
|
+
emitOnUnread(payload)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
init {
|
|
48
|
+
guarded("init") {
|
|
49
|
+
ZellioChat.addListener(chatListener)
|
|
50
|
+
ZellioChat.addUnreadListener(unreadListener)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun invalidate() {
|
|
55
|
+
guarded("invalidate") {
|
|
56
|
+
ZellioChat.removeListener(chatListener)
|
|
57
|
+
ZellioChat.removeUnreadListener(unreadListener)
|
|
58
|
+
failAllTokens("detached")
|
|
59
|
+
}
|
|
60
|
+
super.invalidate()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override fun configure(appKey: String, options: ReadableMap) = guarded("configure") {
|
|
64
|
+
val locale = when (options.string("locale")) {
|
|
65
|
+
"ar" -> ZellioLocale.ARABIC
|
|
66
|
+
"en" -> ZellioLocale.ENGLISH
|
|
67
|
+
else -> ZellioLocale.AUTO
|
|
68
|
+
}
|
|
69
|
+
val theme = ZellioTheme(
|
|
70
|
+
mode = when (options.string("themeMode")) {
|
|
71
|
+
"light" -> ZellioThemeMode.LIGHT
|
|
72
|
+
"dark" -> ZellioThemeMode.DARK
|
|
73
|
+
else -> null
|
|
74
|
+
},
|
|
75
|
+
primaryColor = options.string("primaryColor"),
|
|
76
|
+
customerMessageColor = options.string("customerMessageColor"),
|
|
77
|
+
surfaceColor = options.string("surfaceColor"),
|
|
78
|
+
)
|
|
79
|
+
val logLevel = when (options.string("logLevel")) {
|
|
80
|
+
"none" -> ZellioLogLevel.NONE
|
|
81
|
+
"error" -> ZellioLogLevel.ERROR
|
|
82
|
+
"info" -> ZellioLogLevel.INFO
|
|
83
|
+
"debug" -> ZellioLogLevel.DEBUG
|
|
84
|
+
else -> ZellioLogLevel.WARN
|
|
85
|
+
}
|
|
86
|
+
ZellioChat.configure(
|
|
87
|
+
reactApplicationContext,
|
|
88
|
+
appKey,
|
|
89
|
+
ZellioChatOptions(
|
|
90
|
+
baseUrl = options.string("baseUrl") ?: ZellioChatOptions.DEFAULT_BASE_URL,
|
|
91
|
+
locale = locale,
|
|
92
|
+
theme = theme,
|
|
93
|
+
logLevel = logLevel,
|
|
94
|
+
wrapper = options.string("wrapper"),
|
|
95
|
+
showsInAppBanner = options.bool("showsInAppBanner", true),
|
|
96
|
+
bannerShowsPreview = options.bool("bannerShowsPreview", true),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
// configure usually runs after the first Activity resumed, so tell the SDK which one is on screen.
|
|
100
|
+
ZellioChat.attachActivity(reactApplicationContext.currentActivity)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun identify() = guarded("identify") {
|
|
104
|
+
ZellioChat.identifyWithCallback(ZellioTokenCallbackProvider { callback -> requestToken(callback) })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override fun resolveToken(requestId: String, token: String?, errorCode: String?) = guarded("resolveToken") {
|
|
108
|
+
val callback = pendingTokens.remove(requestId) ?: return@guarded
|
|
109
|
+
when {
|
|
110
|
+
!token.isNullOrEmpty() -> callback.onToken(token)
|
|
111
|
+
// JavaScript says nobody is signed in: the SDK treats a null token as logged out.
|
|
112
|
+
errorCode == "no_token" -> callback.onToken(null)
|
|
113
|
+
else -> callback.onFailure(IllegalStateException(errorCode ?: "unknown"))
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override fun logout() = guarded("logout") { ZellioChat.logout() }
|
|
118
|
+
|
|
119
|
+
override fun present() = guarded("present") { ZellioChat.present() }
|
|
120
|
+
|
|
121
|
+
override fun presentConversation(conversationId: String?) = guarded("presentConversation") {
|
|
122
|
+
ZellioChat.presentConversation(conversationId)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
override fun dismiss() = guarded("dismiss") { ZellioChat.dismiss() }
|
|
126
|
+
|
|
127
|
+
override fun setContext(context: ReadableMap?) = guarded("setContext") {
|
|
128
|
+
ZellioChat.setContext(
|
|
129
|
+
context?.let {
|
|
130
|
+
ZellioContext(
|
|
131
|
+
screen = it.string("screen"),
|
|
132
|
+
itemType = it.string("itemType"),
|
|
133
|
+
itemId = it.string("itemId"),
|
|
134
|
+
itemTitle = it.string("itemTitle"),
|
|
135
|
+
url = it.string("url"),
|
|
136
|
+
)
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
override fun getUnreadCount(promise: Promise) {
|
|
142
|
+
promise.resolve(safe(0) { ZellioChat.currentUnreadCount() }.toDouble())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun setPushToken(token: String, environment: String?) = guarded("setPushToken") {
|
|
146
|
+
ZellioChat.setPushToken(token, environment)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun isZellioPush(payloadJson: String): Boolean = safe(false) { ZellioChat.isZellioPush(payloadJson) }
|
|
150
|
+
|
|
151
|
+
override fun handlePush(payloadJson: String, promise: Promise) {
|
|
152
|
+
promise.resolve(safe(false) { ZellioChat.handlePush(payloadJson) })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override fun handleNotificationTap(payloadJson: String, promise: Promise) {
|
|
156
|
+
promise.resolve(safe(false) { ZellioChat.handleNotificationTap(payloadJson) })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---- token requests: native asks JavaScript, JavaScript answers through resolveToken ----
|
|
160
|
+
|
|
161
|
+
private fun requestToken(callback: ZellioTokenCallbackProvider.Callback) {
|
|
162
|
+
val requestId = UUID.randomUUID().toString()
|
|
163
|
+
pendingTokens[requestId] = callback
|
|
164
|
+
// The first answer wins. This timer is the safety net when JavaScript never answers.
|
|
165
|
+
main.postDelayed({
|
|
166
|
+
pendingTokens.remove(requestId)?.onFailure(IllegalStateException("timeout"))
|
|
167
|
+
}, TOKEN_TIMEOUT_MS)
|
|
168
|
+
val payload = Arguments.createMap()
|
|
169
|
+
payload.putString("requestId", requestId)
|
|
170
|
+
emitOnTokenRequest(payload)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private fun failAllTokens(reason: String) {
|
|
174
|
+
val ids = pendingTokens.keys.toList()
|
|
175
|
+
for (id in ids) pendingTokens.remove(id)?.onFailure(IllegalStateException(reason))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- helpers ----
|
|
179
|
+
|
|
180
|
+
private fun emitEvent(name: String, code: String?) = guarded("event") {
|
|
181
|
+
val payload = Arguments.createMap()
|
|
182
|
+
payload.putString("name", name)
|
|
183
|
+
if (code != null) payload.putString("code", code)
|
|
184
|
+
emitOnEvent(payload)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun ReadableMap.string(key: String): String? =
|
|
188
|
+
if (hasKey(key) && !isNull(key)) getString(key) else null
|
|
189
|
+
|
|
190
|
+
private fun ReadableMap.bool(key: String, default: Boolean): Boolean =
|
|
191
|
+
if (hasKey(key) && !isNull(key)) getBoolean(key) else default
|
|
192
|
+
|
|
193
|
+
private inline fun guarded(name: String, block: () -> Unit) {
|
|
194
|
+
try {
|
|
195
|
+
block()
|
|
196
|
+
} catch (t: Throwable) {
|
|
197
|
+
// The kind of error only. Arguments can hold tokens, so they are never printed.
|
|
198
|
+
Log.w(TAG, "$name failed (${t.javaClass.simpleName})")
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private inline fun <T> safe(fallback: T, block: () -> T): T = try {
|
|
203
|
+
block()
|
|
204
|
+
} catch (t: Throwable) {
|
|
205
|
+
Log.w(TAG, "call failed (${t.javaClass.simpleName})")
|
|
206
|
+
fallback
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
companion object {
|
|
210
|
+
const val NAME = NativeZellioChatSpec.NAME
|
|
211
|
+
private const val TAG = "ZellioChatRN"
|
|
212
|
+
private const val TOKEN_TIMEOUT_MS = 15_000L
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
package ai.zellio.chat.reactnative
|
|
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
|
+
|
|
9
|
+
class ZellioChatPackage : BaseReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
11
|
+
if (name == ZellioChatModule.NAME) ZellioChatModule(reactContext) else null
|
|
12
|
+
|
|
13
|
+
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
|
|
14
|
+
mapOf(
|
|
15
|
+
ZellioChatModule.NAME to ReactModuleInfo(
|
|
16
|
+
ZellioChatModule.NAME,
|
|
17
|
+
ZellioChatModule.NAME,
|
|
18
|
+
false, // canOverrideExistingModule
|
|
19
|
+
false, // needsEagerInit
|
|
20
|
+
false, // isCxxModule
|
|
21
|
+
true, // isTurboModule
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#import "RNZellioChat.h"
|
|
2
|
+
|
|
3
|
+
#if __has_include("ZellioChatReactNative/ZellioChatReactNative-Swift.h")
|
|
4
|
+
#import "ZellioChatReactNative/ZellioChatReactNative-Swift.h"
|
|
5
|
+
#else
|
|
6
|
+
#import "ZellioChatReactNative-Swift.h"
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
@implementation RNZellioChat
|
|
10
|
+
|
|
11
|
+
RCT_EXPORT_MODULE(RNZellioChat)
|
|
12
|
+
|
|
13
|
+
+ (BOOL)requiresMainQueueSetup
|
|
14
|
+
{
|
|
15
|
+
return NO;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
- (instancetype)init
|
|
19
|
+
{
|
|
20
|
+
if ((self = [super init])) {
|
|
21
|
+
__weak RNZellioChat *weakSelf = self;
|
|
22
|
+
[ZCRNBridge attachUnread:^(NSInteger count) {
|
|
23
|
+
[weakSelf emitOnUnread:@{@"count" : @(count)}];
|
|
24
|
+
}
|
|
25
|
+
event:^(NSString *name, NSString * _Nullable code) {
|
|
26
|
+
NSMutableDictionary *payload = [NSMutableDictionary dictionaryWithObject:name forKey:@"name"];
|
|
27
|
+
if (code != nil) {
|
|
28
|
+
payload[@"code"] = code;
|
|
29
|
+
}
|
|
30
|
+
[weakSelf emitOnEvent:payload];
|
|
31
|
+
}
|
|
32
|
+
tokenRequest:^(NSString *requestId) {
|
|
33
|
+
[weakSelf emitOnTokenRequest:@{@"requestId" : requestId}];
|
|
34
|
+
}];
|
|
35
|
+
}
|
|
36
|
+
return self;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
- (void)invalidate
|
|
40
|
+
{
|
|
41
|
+
[ZCRNBridge detach];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
- (void)configure:(NSString *)appKey options:(NSDictionary *)options
|
|
45
|
+
{
|
|
46
|
+
[ZCRNBridge configureWithAppKey:appKey options:options];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
- (void)identify
|
|
50
|
+
{
|
|
51
|
+
[ZCRNBridge identify];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
- (void)resolveToken:(NSString *)requestId token:(NSString * _Nullable)token errorCode:(NSString * _Nullable)errorCode
|
|
55
|
+
{
|
|
56
|
+
[ZCRNBridge resolveTokenWithRequestId:requestId token:token errorCode:errorCode];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (void)logout
|
|
60
|
+
{
|
|
61
|
+
[ZCRNBridge logout];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)present
|
|
65
|
+
{
|
|
66
|
+
[ZCRNBridge present];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (void)presentConversation:(NSString * _Nullable)conversationId
|
|
70
|
+
{
|
|
71
|
+
[ZCRNBridge presentConversationWithId:conversationId];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)dismiss
|
|
75
|
+
{
|
|
76
|
+
[ZCRNBridge dismiss];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)setContext:(NSDictionary * _Nullable)context
|
|
80
|
+
{
|
|
81
|
+
[ZCRNBridge setContext:context];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)getUnreadCount:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
85
|
+
{
|
|
86
|
+
resolve(@([ZCRNBridge unreadCount]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
- (void)setPushToken:(NSString *)token environment:(NSString * _Nullable)environment
|
|
90
|
+
{
|
|
91
|
+
[ZCRNBridge setPushTokenWithHex:token environment:environment];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
- (NSNumber *)isZellioPush:(NSString *)payloadJson
|
|
95
|
+
{
|
|
96
|
+
return @([ZCRNBridge isZellioPushWithJson:payloadJson]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)handlePush:(NSString *)payloadJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
|
|
100
|
+
{
|
|
101
|
+
resolve(@([ZCRNBridge handlePushWithJson:payloadJson]));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)handleNotificationTap:(NSString *)payloadJson
|
|
105
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
106
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
107
|
+
{
|
|
108
|
+
resolve(@([ZCRNBridge handleNotificationTapWithJson:payloadJson]));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
112
|
+
{
|
|
113
|
+
return std::make_shared<facebook::react::NativeZellioChatSpecJSI>(params);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// The only Swift file of this package. It sits between the Objective-C++ module (RNZellioChat.mm) and the
|
|
5
|
+
/// ZeeChat iOS SDK, which is compiled into the same pod from ios/vendor. It holds no chat logic.
|
|
6
|
+
@objc(ZCRNBridge)
|
|
7
|
+
public final class ZCRNBridge: NSObject {
|
|
8
|
+
private static let lock = NSLock()
|
|
9
|
+
private static var unreadToken: Int?
|
|
10
|
+
private static var eventToken: Int?
|
|
11
|
+
private static var onTokenRequest: ((String) -> Void)?
|
|
12
|
+
private static let pending = PendingTokens()
|
|
13
|
+
|
|
14
|
+
/// How long native waits for JavaScript to answer a token request.
|
|
15
|
+
private static let tokenTimeout: TimeInterval = 15
|
|
16
|
+
|
|
17
|
+
// MARK: Events to JavaScript
|
|
18
|
+
|
|
19
|
+
@objc(attachUnread:event:tokenRequest:)
|
|
20
|
+
public static func attach(unread: @escaping (Int) -> Void, event: @escaping (String, String?) -> Void, tokenRequest: @escaping (String) -> Void) {
|
|
21
|
+
detach()
|
|
22
|
+
let unreadToken = ZellioChat.addUnreadListener { count in unread(count) }
|
|
23
|
+
let eventToken = ZellioChat.addEventListener { zellioEvent in event(zellioEvent.name, zellioEvent.code) }
|
|
24
|
+
lock.lock()
|
|
25
|
+
self.unreadToken = unreadToken
|
|
26
|
+
self.eventToken = eventToken
|
|
27
|
+
self.onTokenRequest = tokenRequest
|
|
28
|
+
lock.unlock()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@objc(detach)
|
|
32
|
+
public static func detach() {
|
|
33
|
+
lock.lock()
|
|
34
|
+
let unreadToken = self.unreadToken
|
|
35
|
+
let eventToken = self.eventToken
|
|
36
|
+
self.unreadToken = nil
|
|
37
|
+
self.eventToken = nil
|
|
38
|
+
self.onTokenRequest = nil
|
|
39
|
+
lock.unlock()
|
|
40
|
+
if let token = unreadToken { ZellioChat.removeUnreadListener(token) }
|
|
41
|
+
if let token = eventToken { ZellioChat.removeEventListener(token) }
|
|
42
|
+
pending.failAll()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MARK: Configure and identity
|
|
46
|
+
|
|
47
|
+
@objc(configureWithAppKey:options:)
|
|
48
|
+
public static func configure(appKey: String, options: [String: Any]) {
|
|
49
|
+
var result = ZellioChatOptions()
|
|
50
|
+
if let base = options["baseUrl"] as? String, let url = URL(string: base) { result.baseURL = url }
|
|
51
|
+
switch options["locale"] as? String {
|
|
52
|
+
case "ar"?: result.locale = .arabic
|
|
53
|
+
case "en"?: result.locale = .english
|
|
54
|
+
default: result.locale = .auto
|
|
55
|
+
}
|
|
56
|
+
var theme = ZellioChatTheme()
|
|
57
|
+
switch options["themeMode"] as? String {
|
|
58
|
+
case "light"?: theme.mode = .light
|
|
59
|
+
case "dark"?: theme.mode = .dark
|
|
60
|
+
default: theme.mode = .auto
|
|
61
|
+
}
|
|
62
|
+
theme.primaryColor = color(options["primaryColor"])
|
|
63
|
+
theme.customerMessageColor = color(options["customerMessageColor"])
|
|
64
|
+
theme.surfaceColor = color(options["surfaceColor"])
|
|
65
|
+
result.theme = theme
|
|
66
|
+
switch options["logLevel"] as? String {
|
|
67
|
+
case "none"?: result.logLevel = .none
|
|
68
|
+
case "info"?: result.logLevel = .info
|
|
69
|
+
case "debug"?: result.logLevel = .debug
|
|
70
|
+
default: result.logLevel = .error
|
|
71
|
+
}
|
|
72
|
+
if let value = options["showsInAppBanner"] as? Bool { result.showsInAppBanner = value }
|
|
73
|
+
if let value = options["bannerShowsPreview"] as? Bool { result.bannerShowsPreview = value }
|
|
74
|
+
result.wrapper = options["wrapper"] as? String
|
|
75
|
+
ZellioChat.configure(appKey: appKey, options: result)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@objc(identify)
|
|
79
|
+
public static func identify() {
|
|
80
|
+
ZellioChat.identify { try await requestToken() }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@objc(resolveTokenWithRequestId:token:errorCode:)
|
|
84
|
+
public static func resolveToken(requestId: String, token: String?, errorCode: String?) {
|
|
85
|
+
if let token = token, !token.isEmpty {
|
|
86
|
+
pending.finish(requestId, .success(token))
|
|
87
|
+
} else {
|
|
88
|
+
pending.finish(requestId, .failure(ZCRNError.rejected(errorCode ?? "no_token")))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@objc(logout)
|
|
93
|
+
public static func logout() { ZellioChat.logout() }
|
|
94
|
+
|
|
95
|
+
// MARK: Presenting and context
|
|
96
|
+
|
|
97
|
+
@objc(present)
|
|
98
|
+
public static func present() { ZellioChat.present() }
|
|
99
|
+
|
|
100
|
+
@objc(presentConversationWithId:)
|
|
101
|
+
public static func presentConversation(id: String?) { ZellioChat.presentConversation(id: id) }
|
|
102
|
+
|
|
103
|
+
@objc(dismiss)
|
|
104
|
+
public static func dismiss() { ZellioChat.dismiss() }
|
|
105
|
+
|
|
106
|
+
@objc(setContext:)
|
|
107
|
+
public static func setContext(_ context: [String: Any]?) {
|
|
108
|
+
guard let context = context else {
|
|
109
|
+
ZellioChat.setContext(nil)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
ZellioChat.setContext(ZellioChatContext(
|
|
113
|
+
screen: context["screen"] as? String,
|
|
114
|
+
itemType: context["itemType"] as? String,
|
|
115
|
+
itemId: context["itemId"] as? String,
|
|
116
|
+
itemTitle: context["itemTitle"] as? String,
|
|
117
|
+
url: context["url"] as? String
|
|
118
|
+
))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@objc(unreadCount)
|
|
122
|
+
public static func unreadCount() -> Int { ZellioChat.unreadCount }
|
|
123
|
+
|
|
124
|
+
// MARK: Push
|
|
125
|
+
|
|
126
|
+
@objc(setPushTokenWithHex:environment:)
|
|
127
|
+
public static func setPushToken(hex: String, environment: String?) {
|
|
128
|
+
let env = (environment == "sandbox" || environment == "production") ? environment : nil
|
|
129
|
+
ZellioChat.setPushToken(hex: hex, environment: env)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@objc(isZellioPushWithJson:)
|
|
133
|
+
public static func isZellioPush(json: String) -> Bool {
|
|
134
|
+
guard let payload = object(json) else { return false }
|
|
135
|
+
return ZellioChat.isZellioPush(payload)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@objc(handlePushWithJson:)
|
|
139
|
+
public static func handlePush(json: String) -> Bool {
|
|
140
|
+
guard let payload = object(json) else { return false }
|
|
141
|
+
return ZellioChat.handlePush(payload)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@objc(handleNotificationTapWithJson:)
|
|
145
|
+
public static func handleNotificationTap(json: String) -> Bool {
|
|
146
|
+
guard let payload = object(json) else { return false }
|
|
147
|
+
return ZellioChat.handleNotificationTap(payload)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// MARK: Helpers
|
|
151
|
+
|
|
152
|
+
private static func object(_ json: String) -> [AnyHashable: Any]? {
|
|
153
|
+
guard let data = json.data(using: .utf8),
|
|
154
|
+
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return nil }
|
|
155
|
+
return parsed
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// `#RRGGBB` only. Anything else is ignored and the Studio color stays.
|
|
159
|
+
private static func color(_ value: Any?) -> UIColor? {
|
|
160
|
+
guard var text = value as? String else { return nil }
|
|
161
|
+
if text.hasPrefix("#") { text.removeFirst() }
|
|
162
|
+
guard text.count == 6, let number = UInt32(text, radix: 16) else { return nil }
|
|
163
|
+
return UIColor(red: CGFloat((number >> 16) & 0xFF) / 255, green: CGFloat((number >> 8) & 0xFF) / 255,
|
|
164
|
+
blue: CGFloat(number & 0xFF) / 255, alpha: 1)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// The SDK calls this when it needs a session. JavaScript answers through `resolveToken`.
|
|
168
|
+
/// If JavaScript does not answer in 15 seconds, the request fails. The token is never logged.
|
|
169
|
+
private static func requestToken() async throws -> String {
|
|
170
|
+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, Error>) in
|
|
171
|
+
lock.lock()
|
|
172
|
+
let emit = onTokenRequest
|
|
173
|
+
lock.unlock()
|
|
174
|
+
guard let emit = emit else {
|
|
175
|
+
continuation.resume(throwing: ZCRNError.rejected("no_listener"))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
let requestId = UUID().uuidString
|
|
179
|
+
pending.add(requestId, continuation)
|
|
180
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + tokenTimeout) {
|
|
181
|
+
pending.finish(requestId, .failure(ZCRNError.rejected("timeout")))
|
|
182
|
+
}
|
|
183
|
+
emit(requestId)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
enum ZCRNError: Error {
|
|
189
|
+
/// The code is a short fixed word such as "timeout" or "no_token". Never a token.
|
|
190
|
+
case rejected(String)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Token requests that wait for JavaScript. Each one finishes once: the first answer or the timeout wins.
|
|
194
|
+
private final class PendingTokens {
|
|
195
|
+
private let lock = NSLock()
|
|
196
|
+
private var waiting: [String: CheckedContinuation<String, Error>] = [:]
|
|
197
|
+
|
|
198
|
+
func add(_ id: String, _ continuation: CheckedContinuation<String, Error>) {
|
|
199
|
+
lock.lock(); defer { lock.unlock() }
|
|
200
|
+
waiting[id] = continuation
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func finish(_ id: String, _ result: Result<String, Error>) {
|
|
204
|
+
lock.lock()
|
|
205
|
+
let continuation = waiting.removeValue(forKey: id)
|
|
206
|
+
lock.unlock()
|
|
207
|
+
continuation?.resume(with: result)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func failAll() {
|
|
211
|
+
lock.lock()
|
|
212
|
+
let all = waiting
|
|
213
|
+
waiting.removeAll()
|
|
214
|
+
lock.unlock()
|
|
215
|
+
for (_, continuation) in all { continuation.resume(throwing: ZCRNError.rejected("detached")) }
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSPrivacyTracking</key>
|
|
6
|
+
<false/>
|
|
7
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
10
|
+
<array>
|
|
11
|
+
<dict>
|
|
12
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
13
|
+
<string>NSPrivacyCollectedDataTypeEmailAddress</string>
|
|
14
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
15
|
+
<true/>
|
|
16
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
17
|
+
<false/>
|
|
18
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
19
|
+
<array>
|
|
20
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
21
|
+
</array>
|
|
22
|
+
</dict>
|
|
23
|
+
<dict>
|
|
24
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
25
|
+
<string>NSPrivacyCollectedDataTypePhoneNumber</string>
|
|
26
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
27
|
+
<true/>
|
|
28
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
29
|
+
<false/>
|
|
30
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
31
|
+
<array>
|
|
32
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
33
|
+
</array>
|
|
34
|
+
</dict>
|
|
35
|
+
<dict>
|
|
36
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
37
|
+
<string>NSPrivacyCollectedDataTypeName</string>
|
|
38
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
39
|
+
<true/>
|
|
40
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
41
|
+
<false/>
|
|
42
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
43
|
+
<array>
|
|
44
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
45
|
+
</array>
|
|
46
|
+
</dict>
|
|
47
|
+
<dict>
|
|
48
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
49
|
+
<string>NSPrivacyCollectedDataTypeOtherUserContent</string>
|
|
50
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
51
|
+
<true/>
|
|
52
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
53
|
+
<false/>
|
|
54
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
55
|
+
<array>
|
|
56
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
57
|
+
</array>
|
|
58
|
+
</dict>
|
|
59
|
+
<dict>
|
|
60
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
61
|
+
<string>NSPrivacyCollectedDataTypeUserID</string>
|
|
62
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
63
|
+
<true/>
|
|
64
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
65
|
+
<false/>
|
|
66
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
67
|
+
<array>
|
|
68
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
69
|
+
</array>
|
|
70
|
+
</dict>
|
|
71
|
+
<dict>
|
|
72
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
73
|
+
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
|
74
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
75
|
+
<true/>
|
|
76
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
77
|
+
<false/>
|
|
78
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
79
|
+
<array>
|
|
80
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
81
|
+
</array>
|
|
82
|
+
</dict>
|
|
83
|
+
<dict>
|
|
84
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
85
|
+
<string>NSPrivacyCollectedDataTypePhotosorVideos</string>
|
|
86
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
87
|
+
<true/>
|
|
88
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
89
|
+
<false/>
|
|
90
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
91
|
+
<array>
|
|
92
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
93
|
+
</array>
|
|
94
|
+
</dict>
|
|
95
|
+
</array>
|
|
96
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
97
|
+
<array/>
|
|
98
|
+
</dict>
|
|
99
|
+
</plist>
|