@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,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
|
+
}
|