@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,389 @@
1
+ import Foundation
2
+
3
+ public enum ChatLocale: String, Codable, Equatable, Sendable {
4
+ case ar
5
+ case en
6
+
7
+ public var isRTL: Bool { self == .ar }
8
+ public var other: ChatLocale { self == .ar ? .en : .ar }
9
+ }
10
+
11
+ public struct LocalizedText: Codable, Equatable, Sendable {
12
+ public var ar: String?
13
+ public var en: String?
14
+
15
+ public init(ar: String? = nil, en: String? = nil) {
16
+ self.ar = ar
17
+ self.en = en
18
+ }
19
+
20
+ private enum CodingKeys: String, CodingKey { case ar, en }
21
+
22
+ public init(from decoder: Decoder) throws {
23
+ guard let container = try? decoder.container(keyedBy: CodingKeys.self) else {
24
+ self.init()
25
+ return
26
+ }
27
+ self.init(ar: container.lenient(String.self, .ar), en: container.lenient(String.self, .en))
28
+ }
29
+
30
+ /// The text for a locale. Falls back to the other language when one is empty.
31
+ public func resolve(_ locale: ChatLocale) -> String? {
32
+ let first = locale == .ar ? ar : en
33
+ let second = locale == .ar ? en : ar
34
+ if let first = first, !first.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return first }
35
+ if let second = second, !second.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return second }
36
+ return nil
37
+ }
38
+ }
39
+
40
+ /// Only https, mailto and tel are opened. Never anything else.
41
+ public enum LinkPolicy {
42
+ public static func isSafe(_ text: String) -> Bool {
43
+ let lower = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
44
+ if lower.hasPrefix("https://") { return URL(string: lower) != nil && lower.count > 8 }
45
+ if lower.hasPrefix("mailto:") || lower.hasPrefix("tel:") { return lower.count > 7 || lower.hasPrefix("tel:") && lower.count > 4 }
46
+ return false
47
+ }
48
+ }
49
+
50
+ public enum ThemeMode: String, Equatable, Sendable { case auto, light, dark }
51
+ public enum Density: String, Equatable, Sendable { case comfortable, compact }
52
+ public enum Corners: String, Equatable, Sendable { case rounded, square }
53
+ public enum HeaderStyle: String, Equatable, Sendable { case gradient, solid, light }
54
+ public enum ChatLayout: String, Equatable, Sendable {
55
+ case chatOnly = "chat-only"
56
+ case support
57
+ case faqFirst = "faq-first"
58
+ }
59
+
60
+ public struct Appearance: Equatable, Sendable {
61
+ public var brandName: String = ""
62
+ public var logoAlt: LocalizedText = LocalizedText()
63
+ public var primaryColor: String = "#7D3BED"
64
+ public var customerMessageColor: String = "#7D3BED"
65
+ public var surfaceColor: String = "#FFFFFF"
66
+ public var theme: ThemeMode = .auto
67
+ public var density: Density = .comfortable
68
+ public var corners: Corners = .rounded
69
+ public var headerStyle: HeaderStyle = .gradient
70
+ public var showAgentAvatar: Bool = true
71
+ public var showAttribution: Bool = true
72
+ public var soundEnabled: Bool = false
73
+ public var unreadBadge: Bool = true
74
+
75
+ public init() {}
76
+
77
+ private enum CodingKeys: String, CodingKey {
78
+ case brandName, logoAlt, primaryColor, customerMessageColor, surfaceColor, theme, density
79
+ case corners, headerStyle, showAgentAvatar, showAttribution, soundEnabled, unreadBadge
80
+ }
81
+
82
+ init(decoding decoder: Decoder) {
83
+ guard let c = try? decoder.container(keyedBy: CodingKeys.self) else { return }
84
+ brandName = c.lenient(String.self, .brandName) ?? ""
85
+ logoAlt = c.lenient(LocalizedText.self, .logoAlt) ?? LocalizedText()
86
+ primaryColor = HexColor(c.lenient(String.self, .primaryColor) ?? "")?.hex ?? primaryColor
87
+ customerMessageColor = HexColor(c.lenient(String.self, .customerMessageColor) ?? "")?.hex ?? primaryColor
88
+ surfaceColor = HexColor(c.lenient(String.self, .surfaceColor) ?? "")?.hex ?? surfaceColor
89
+ theme = ThemeMode(rawValue: c.lenient(String.self, .theme) ?? "") ?? .auto
90
+ density = Density(rawValue: c.lenient(String.self, .density) ?? "") ?? .comfortable
91
+ corners = Corners(rawValue: c.lenient(String.self, .corners) ?? "") ?? .rounded
92
+ headerStyle = HeaderStyle(rawValue: c.lenient(String.self, .headerStyle) ?? "") ?? .gradient
93
+ showAgentAvatar = c.lenient(Bool.self, .showAgentAvatar) ?? true
94
+ showAttribution = c.lenient(Bool.self, .showAttribution) ?? true
95
+ soundEnabled = c.lenient(Bool.self, .soundEnabled) ?? false
96
+ unreadBadge = c.lenient(Bool.self, .unreadBadge) ?? true
97
+ }
98
+ }
99
+
100
+ public struct ContactLink: Equatable, Sendable {
101
+ public var label: LocalizedText
102
+ public var href: String
103
+ }
104
+
105
+ public struct ModuleVisibility: Equatable, Sendable {
106
+ public var locales: [ChatLocale]?
107
+ public var availability: String?
108
+ public var customer: String?
109
+ public init(locales: [ChatLocale]? = nil, availability: String? = nil, customer: String? = nil) {
110
+ self.locales = locales
111
+ self.availability = availability
112
+ self.customer = customer
113
+ }
114
+ }
115
+
116
+ public enum HomeModuleKind: String, Equatable, Sendable {
117
+ case greeting
118
+ case announcement
119
+ case startConversation = "start-conversation"
120
+ case externalLink = "external-link"
121
+ case contactLinks = "contact-links"
122
+ }
123
+
124
+ public struct HomeModule: Equatable, Sendable, Identifiable {
125
+ public var id: String
126
+ public var kind: HomeModuleKind
127
+ public var label: LocalizedText
128
+ public var description: LocalizedText
129
+ public var primary: Bool
130
+ public var href: String?
131
+ public var links: [ContactLink]
132
+ public var visibility: ModuleVisibility
133
+
134
+ public init(id: String, kind: HomeModuleKind, label: LocalizedText = LocalizedText(), description: LocalizedText = LocalizedText(),
135
+ primary: Bool = false, href: String? = nil, links: [ContactLink] = [], visibility: ModuleVisibility = ModuleVisibility()) {
136
+ self.id = id
137
+ self.kind = kind
138
+ self.label = label
139
+ self.description = description
140
+ self.primary = primary
141
+ self.href = href
142
+ self.links = links
143
+ self.visibility = visibility
144
+ }
145
+
146
+ /// Hidden by locale, availability or customer visibility.
147
+ public func isVisible(locale: ChatLocale, online: Bool, verified: Bool) -> Bool {
148
+ if let locales = visibility.locales, !locales.contains(locale) { return false }
149
+ if let availability = visibility.availability {
150
+ if availability == "online" && !online { return false }
151
+ if availability == "offline" && online { return false }
152
+ }
153
+ if let customer = visibility.customer {
154
+ if customer == "verified" && !verified { return false }
155
+ if customer == "anonymous" && verified { return false }
156
+ }
157
+ return true
158
+ }
159
+ }
160
+
161
+ private struct RawModule: Decodable {
162
+ var id: String?
163
+ var type: String?
164
+ var label: LocalizedText?
165
+ var description: LocalizedText?
166
+ var primary: Bool?
167
+ var href: String?
168
+ var disabled: Bool?
169
+ var links: [RawLink]?
170
+ var visibility: RawVisibility?
171
+
172
+ private enum CodingKeys: String, CodingKey { case id, type, label, description, primary, href, disabled, links, visibility }
173
+
174
+ init(from decoder: Decoder) throws {
175
+ let c = try decoder.container(keyedBy: CodingKeys.self)
176
+ id = c.lenient(String.self, .id)
177
+ type = c.lenient(String.self, .type)
178
+ label = c.lenient(LocalizedText.self, .label)
179
+ description = c.lenient(LocalizedText.self, .description)
180
+ primary = c.lenient(Bool.self, .primary)
181
+ href = c.lenient(String.self, .href)
182
+ disabled = c.lenient(Bool.self, .disabled)
183
+ links = c.lenient(LossyArray<RawLink>.self, .links)?.items
184
+ visibility = c.lenient(RawVisibility.self, .visibility)
185
+ }
186
+
187
+ /// Nil for an unknown type, a disabled module or a module without its required fields.
188
+ func build() -> HomeModule? {
189
+ guard let type = type, let kind = HomeModuleKind(rawValue: type), disabled != true else { return nil }
190
+ let visible = ModuleVisibility(
191
+ locales: visibility?.locales?.compactMap { ChatLocale(rawValue: $0) },
192
+ availability: visibility?.availability,
193
+ customer: visibility?.customer
194
+ )
195
+ var module = HomeModule(
196
+ id: id ?? UUID().uuidString, kind: kind, label: label ?? LocalizedText(),
197
+ description: description ?? LocalizedText(), primary: primary ?? false, visibility: visible
198
+ )
199
+ switch kind {
200
+ case .externalLink:
201
+ guard let href = href, LinkPolicy.isSafe(href) else { return nil }
202
+ module.href = href
203
+ case .contactLinks:
204
+ module.links = (links ?? []).compactMap { link in
205
+ guard let href = link.href, LinkPolicy.isSafe(href) else { return nil }
206
+ return ContactLink(label: link.label ?? LocalizedText(), href: href)
207
+ }
208
+ if module.links.isEmpty { return nil }
209
+ default:
210
+ break
211
+ }
212
+ return module
213
+ }
214
+ }
215
+
216
+ private struct RawLink: Decodable {
217
+ var label: LocalizedText?
218
+ var href: String?
219
+ private enum CodingKeys: String, CodingKey { case label, href }
220
+ init(from decoder: Decoder) throws {
221
+ let c = try decoder.container(keyedBy: CodingKeys.self)
222
+ label = c.lenient(LocalizedText.self, .label)
223
+ href = c.lenient(String.self, .href)
224
+ }
225
+ }
226
+
227
+ private struct RawVisibility: Decodable {
228
+ var locales: [String]?
229
+ var availability: String?
230
+ var customer: String?
231
+ private enum CodingKeys: String, CodingKey { case locales, availability, customer }
232
+ init(from decoder: Decoder) throws {
233
+ let c = try decoder.container(keyedBy: CodingKeys.self)
234
+ locales = c.lenient([String].self, .locales)
235
+ availability = c.lenient(String.self, .availability)
236
+ customer = c.lenient(String.self, .customer)
237
+ }
238
+ }
239
+
240
+ public enum LanguagePolicy: Equatable, Sendable {
241
+ case single(ChatLocale)
242
+ case both(defaultLocale: ChatLocale, startIn: String)
243
+ }
244
+
245
+ public struct ScreenTexts: Equatable, Sendable {
246
+ public var heading = LocalizedText()
247
+ public var greeting = LocalizedText()
248
+ public var description = LocalizedText()
249
+ public var offline = LocalizedText()
250
+ public init() {}
251
+ }
252
+
253
+ public struct Behavior: Equatable, Sendable {
254
+ public var allowAttachments = true
255
+ public var replyExpectation = LocalizedText()
256
+ public var consentRequired = false
257
+ public var consentText = LocalizedText()
258
+ public var privacyUrl: String?
259
+ public init() {}
260
+ }
261
+
262
+ public struct SiteSnapshot: Equatable, Sendable {
263
+ public var siteId: String = ""
264
+ public var versionId: String = ""
265
+ public var layout: ChatLayout = .support
266
+ public var language: LanguagePolicy = .both(defaultLocale: .en, startIn: "browser")
267
+ public var appearance = Appearance()
268
+ public var modules: [HomeModule] = []
269
+ public var screens = ScreenTexts()
270
+ public var behavior = Behavior()
271
+ public init() {}
272
+ }
273
+
274
+ public struct ChatConfig: Equatable, Sendable {
275
+ public var snapshot = SiteSnapshot()
276
+ public var logoUrl: String?
277
+ public var online: Bool = true
278
+ public var allowGuests: Bool = true
279
+ public var minSdkVersion: String?
280
+ public init() {}
281
+
282
+ public var versionId: String { snapshot.versionId }
283
+
284
+ /// Home modules that this SDK draws, in snapshot order, for this viewer.
285
+ public func homeModules(locale: ChatLocale, verified: Bool) -> [HomeModule] {
286
+ snapshot.modules.filter { $0.isVisible(locale: locale, online: online, verified: verified) }
287
+ }
288
+
289
+ public func consentText(locale: ChatLocale) -> String {
290
+ snapshot.behavior.consentText.resolve(locale) ?? ChatStrings(locale: locale).string("consent.default")
291
+ }
292
+
293
+ /// True when this SDK version is older than the app's minimum.
294
+ public var sdkTooOld: Bool {
295
+ guard let minimum = minSdkVersion, !minimum.isEmpty else { return false }
296
+ return SemVer.compare(SDKInfo.version, minimum) < 0
297
+ }
298
+
299
+ /// Reads the full response body: `{ "data": { ... } }`. Also accepts the inner object alone.
300
+ public static func parse(_ body: Data) throws -> ChatConfig {
301
+ let root = try JSONSerialization.jsonObject(with: body, options: [.fragmentsAllowed])
302
+ guard let object = root as? [String: Any] else { throw ChatAPIError.decoding }
303
+ let inner = (object["data"] as? [String: Any]) ?? object
304
+ let data = try JSONSerialization.data(withJSONObject: inner)
305
+ let raw = try JSONDecoder().decode(RawConfig.self, from: data)
306
+ return raw.build()
307
+ }
308
+ }
309
+
310
+ private struct RawConfig: Decodable {
311
+ var snapshot: RawSnapshot?
312
+ var logoUrl: String?
313
+ var online: Bool?
314
+ var allowGuests: Bool?
315
+ var minSdkVersion: String?
316
+
317
+ private enum CodingKeys: String, CodingKey { case snapshot, assets, availability, app }
318
+ private enum AssetKeys: String, CodingKey { case logoUrl }
319
+ private enum AvailabilityKeys: String, CodingKey { case online }
320
+ private enum AppKeys: String, CodingKey { case allowGuests, minSdkVersion }
321
+
322
+ init(from decoder: Decoder) throws {
323
+ let c = try decoder.container(keyedBy: CodingKeys.self)
324
+ snapshot = c.lenient(RawSnapshot.self, .snapshot)
325
+ if let a = try? c.nestedContainer(keyedBy: AssetKeys.self, forKey: .assets) { logoUrl = a.lenient(String.self, .logoUrl) }
326
+ if let a = try? c.nestedContainer(keyedBy: AvailabilityKeys.self, forKey: .availability) { online = a.lenient(Bool.self, .online) }
327
+ if let a = try? c.nestedContainer(keyedBy: AppKeys.self, forKey: .app) {
328
+ allowGuests = a.lenient(Bool.self, .allowGuests)
329
+ minSdkVersion = a.lenient(String.self, .minSdkVersion)
330
+ }
331
+ }
332
+
333
+ func build() -> ChatConfig {
334
+ var config = ChatConfig()
335
+ config.snapshot = snapshot?.build() ?? SiteSnapshot()
336
+ config.logoUrl = logoUrl
337
+ config.online = online ?? true
338
+ config.allowGuests = allowGuests ?? true
339
+ config.minSdkVersion = minSdkVersion
340
+ return config
341
+ }
342
+ }
343
+
344
+ private struct RawSnapshot: Decodable {
345
+ var built = SiteSnapshot()
346
+
347
+ private enum CodingKeys: String, CodingKey {
348
+ case siteId, versionId, layout, language, appearance, modules, screens, behavior
349
+ }
350
+ private enum LanguageKeys: String, CodingKey { case mode, locale, defaultLocale, startIn }
351
+ private enum ScreenKeys: String, CodingKey { case heading, greeting, description, offline }
352
+ private enum BehaviorKeys: String, CodingKey {
353
+ case allowAttachments, replyExpectation, consentRequired, consentText, privacyUrl
354
+ }
355
+
356
+ init(from decoder: Decoder) throws {
357
+ let c = try decoder.container(keyedBy: CodingKeys.self)
358
+ built.siteId = c.lenient(String.self, .siteId) ?? ""
359
+ built.versionId = c.lenient(String.self, .versionId) ?? ""
360
+ built.layout = ChatLayout(rawValue: c.lenient(String.self, .layout) ?? "") ?? .support
361
+ if let l = try? c.nestedContainer(keyedBy: LanguageKeys.self, forKey: .language) {
362
+ let mode = l.lenient(String.self, .mode) ?? "both"
363
+ if mode == "single", let locale = ChatLocale(rawValue: l.lenient(String.self, .locale) ?? "") {
364
+ built.language = .single(locale)
365
+ } else {
366
+ let fallback = ChatLocale(rawValue: l.lenient(String.self, .defaultLocale) ?? "") ?? .en
367
+ built.language = .both(defaultLocale: fallback, startIn: l.lenient(String.self, .startIn) ?? "browser")
368
+ }
369
+ }
370
+ if let a = try? c.superDecoder(forKey: .appearance) { built.appearance = Appearance(decoding: a) }
371
+ built.modules = (c.lenient(LossyArray<RawModule>.self, .modules)?.items ?? []).compactMap { $0.build() }
372
+ if let s = try? c.nestedContainer(keyedBy: ScreenKeys.self, forKey: .screens) {
373
+ built.screens.heading = s.lenient(LocalizedText.self, .heading) ?? LocalizedText()
374
+ built.screens.greeting = s.lenient(LocalizedText.self, .greeting) ?? LocalizedText()
375
+ built.screens.description = s.lenient(LocalizedText.self, .description) ?? LocalizedText()
376
+ built.screens.offline = s.lenient(LocalizedText.self, .offline) ?? LocalizedText()
377
+ }
378
+ if let b = try? c.nestedContainer(keyedBy: BehaviorKeys.self, forKey: .behavior) {
379
+ built.behavior.allowAttachments = b.lenient(Bool.self, .allowAttachments) ?? true
380
+ built.behavior.replyExpectation = b.lenient(LocalizedText.self, .replyExpectation) ?? LocalizedText()
381
+ built.behavior.consentRequired = b.lenient(Bool.self, .consentRequired) ?? false
382
+ built.behavior.consentText = b.lenient(LocalizedText.self, .consentText) ?? LocalizedText()
383
+ let url = b.lenient(String.self, .privacyUrl)
384
+ built.behavior.privacyUrl = (url != nil && LinkPolicy.isSafe(url ?? "")) ? url : nil
385
+ }
386
+ }
387
+
388
+ func build() -> SiteSnapshot { built }
389
+ }