@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.
Files changed (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +239 -2
  3. package/ZellioChatReactNative.podspec +30 -0
  4. package/android/build.gradle +62 -0
  5. package/android/src/main/AndroidManifest.xml +1 -0
  6. package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatModule.kt +214 -0
  7. package/android/src/main/java/ai/zellio/chat/reactnative/ZellioChatPackage.kt +25 -0
  8. package/app.plugin.js +4 -0
  9. package/ios/RNZellioChat.h +5 -0
  10. package/ios/RNZellioChat.mm +116 -0
  11. package/ios/ZCRNBridge.swift +217 -0
  12. package/ios/vendor/resources/PrivacyInfo.xcprivacy +99 -0
  13. package/ios/vendor/swift/ZellioChat/Model/ChatModel.swift +261 -0
  14. package/ios/vendor/swift/ZellioChat/Platform/Platform.swift +124 -0
  15. package/ios/vendor/swift/ZellioChat/Platform/URLSessionTransport.swift +175 -0
  16. package/ios/vendor/swift/ZellioChat/Preview/Previews.swift +62 -0
  17. package/ios/vendor/swift/ZellioChat/Public/InAppBanner.swift +155 -0
  18. package/ios/vendor/swift/ZellioChat/Public/PublicTypes.swift +141 -0
  19. package/ios/vendor/swift/ZellioChat/Public/Runtime.swift +406 -0
  20. package/ios/vendor/swift/ZellioChat/Public/ZellioChat.swift +209 -0
  21. package/ios/vendor/swift/ZellioChat/Theme/ChatTheme.swift +185 -0
  22. package/ios/vendor/swift/ZellioChat/Thread/AttachmentPicker.swift +354 -0
  23. package/ios/vendor/swift/ZellioChat/Thread/ComposerBar.swift +355 -0
  24. package/ios/vendor/swift/ZellioChat/Thread/ThreadCells.swift +776 -0
  25. package/ios/vendor/swift/ZellioChat/Thread/ThreadViewController.swift +703 -0
  26. package/ios/vendor/swift/ZellioChat/Views/ChatRootView.swift +154 -0
  27. package/ios/vendor/swift/ZellioChat/Views/Components.swift +335 -0
  28. package/ios/vendor/swift/ZellioChat/Views/HomeView.swift +413 -0
  29. package/ios/vendor/swift/ZellioChatCore/ChatModels.swift +373 -0
  30. package/ios/vendor/swift/ZellioChatCore/ConfigModels.swift +389 -0
  31. package/ios/vendor/swift/ZellioChatCore/Engine.swift +325 -0
  32. package/ios/vendor/swift/ZellioChatCore/Errors.swift +158 -0
  33. package/ios/vendor/swift/ZellioChatCore/Generated/ChatStringTable.swift +125 -0
  34. package/ios/vendor/swift/ZellioChatCore/Outbox.swift +223 -0
  35. package/ios/vendor/swift/ZellioChatCore/Push.swift +74 -0
  36. package/ios/vendor/swift/ZellioChatCore/Realtime.swift +328 -0
  37. package/ios/vendor/swift/ZellioChatCore/Session.swift +210 -0
  38. package/ios/vendor/swift/ZellioChatCore/Storage.swift +201 -0
  39. package/ios/vendor/swift/ZellioChatCore/Strings.swift +104 -0
  40. package/ios/vendor/swift/ZellioChatCore/Theme.swift +238 -0
  41. package/ios/vendor/swift/ZellioChatCore/ThreadController.swift +319 -0
  42. package/ios/vendor/swift/ZellioChatCore/Timeline.swift +157 -0
  43. package/ios/vendor/swift/ZellioChatCore/Transport.swift +236 -0
  44. package/ios/vendor/swift/ZellioChatCore/Util.swift +137 -0
  45. package/lib/NativeZellioChat.d.ts +34 -0
  46. package/lib/NativeZellioChat.js +4 -0
  47. package/lib/index.d.ts +37 -0
  48. package/lib/index.js +241 -0
  49. package/lib/types.d.ts +54 -0
  50. package/lib/types.js +2 -0
  51. package/lib/version.d.ts +4 -0
  52. package/lib/version.js +7 -0
  53. package/package.json +75 -4
  54. package/plugin/build/index.js +73 -0
  55. package/src/NativeZellioChat.ts +30 -0
  56. package/src/index.ts +237 -0
  57. package/src/types.ts +56 -0
  58. 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,4 @@
1
+ // Expo loads this file for `plugins: ["@zellio/chat-react-native"]`. The TypeScript source is plugin/src;
2
+ // `npm run build` (and every `npm pack`) compiles it to plugin/build.
3
+ const built = require('./plugin/build');
4
+ module.exports = built.default ?? built;
@@ -0,0 +1,5 @@
1
+ #import <RNZellioChatSpec/RNZellioChatSpec.h>
2
+
3
+ /// The Turbo Native Module. It only forwards to ZCRNBridge (Swift), which calls the ZeeChat iOS SDK.
4
+ @interface RNZellioChat : NativeZellioChatSpecBase <NativeZellioChatSpec>
5
+ @end
@@ -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>