@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,155 @@
1
+ #if canImport(SwiftUI) && canImport(UIKit)
2
+ import UIKit
3
+
4
+ /// The small banner for a new message while the chat is closed and the app is open.
5
+ /// It hides after 4 s, swipe up closes it, a tap opens the chat.
6
+ final class InAppBannerController {
7
+ private var view: BannerView?
8
+ private var hideTask: DispatchWorkItem?
9
+
10
+ func show(name: String, brand: String, text: String, avatarURL: String?, theme: ChatTheme, in window: UIWindow?, onTap: @escaping () -> Void) {
11
+ guard let window = window else { return }
12
+ hide(animated: false)
13
+ let banner = BannerView(name: name, text: text, avatarURL: avatarURL, theme: theme)
14
+ banner.semanticContentAttribute = theme.isRTL ? .forceRightToLeft : .forceLeftToRight
15
+ banner.onTap = { [weak self] in
16
+ self?.hide(animated: true)
17
+ onTap()
18
+ }
19
+ banner.onSwipeUp = { [weak self] in self?.hide(animated: true) }
20
+ banner.translatesAutoresizingMaskIntoConstraints = false
21
+ window.addSubview(banner)
22
+ let guide = window.safeAreaLayoutGuide
23
+ NSLayoutConstraint.activate([
24
+ banner.topAnchor.constraint(equalTo: guide.topAnchor, constant: 8),
25
+ banner.centerXAnchor.constraint(equalTo: window.centerXAnchor),
26
+ banner.widthAnchor.constraint(lessThanOrEqualToConstant: 520),
27
+ banner.leadingAnchor.constraint(greaterThanOrEqualTo: guide.leadingAnchor, constant: 12),
28
+ banner.trailingAnchor.constraint(lessThanOrEqualTo: guide.trailingAnchor, constant: -12),
29
+ ])
30
+ // On a phone the banner fills the width between the margins.
31
+ let wide = banner.leadingAnchor.constraint(equalTo: guide.leadingAnchor, constant: 12)
32
+ wide.priority = .defaultHigh
33
+ wide.isActive = true
34
+ window.layoutIfNeeded()
35
+ view = banner
36
+ let reduced = UIAccessibility.isReduceMotionEnabled
37
+ banner.alpha = 0
38
+ banner.transform = reduced ? .identity : CGAffineTransform(translationX: 0, y: -banner.bounds.height - 20)
39
+ UIView.animate(withDuration: reduced ? 0.01 : 0.28, delay: 0, options: .curveEaseOut) {
40
+ banner.alpha = 1
41
+ banner.transform = .identity
42
+ }
43
+ UIAccessibility.post(notification: .announcement, argument: "\(name). \(text)")
44
+ let work = DispatchWorkItem { [weak self] in self?.hide(animated: true) }
45
+ hideTask = work
46
+ DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: work)
47
+ }
48
+
49
+ func hide(animated: Bool) {
50
+ hideTask?.cancel()
51
+ hideTask = nil
52
+ guard let banner = view else { return }
53
+ view = nil
54
+ guard animated else {
55
+ banner.removeFromSuperview()
56
+ return
57
+ }
58
+ let reduced = UIAccessibility.isReduceMotionEnabled
59
+ UIView.animate(withDuration: reduced ? 0.01 : 0.22, animations: {
60
+ banner.alpha = 0
61
+ if !reduced { banner.transform = CGAffineTransform(translationX: 0, y: -banner.bounds.height - 20) }
62
+ }, completion: { _ in banner.removeFromSuperview() })
63
+ }
64
+ }
65
+
66
+ private final class BannerView: UIView {
67
+ var onTap: () -> Void = {}
68
+ var onSwipeUp: () -> Void = {}
69
+
70
+ init(name: String, text: String, avatarURL: String?, theme: ChatTheme) {
71
+ super.init(frame: .zero)
72
+ let palette = theme.palette
73
+ backgroundColor = theme.uiColor(palette.bg)
74
+ layer.cornerRadius = 16
75
+ layer.shadowColor = UIColor.black.cgColor
76
+ layer.shadowOpacity = 0.18
77
+ layer.shadowRadius = 14
78
+ layer.shadowOffset = CGSize(width: 0, height: 4)
79
+ layer.borderWidth = 1
80
+ layer.borderColor = theme.uiColor(palette.line).cgColor
81
+
82
+ let avatar = UIImageView()
83
+ avatar.backgroundColor = theme.uiColor(palette.avatarTone)
84
+ avatar.layer.cornerRadius = 18
85
+ avatar.clipsToBounds = true
86
+ avatar.contentMode = .scaleAspectFill
87
+ avatar.translatesAutoresizingMaskIntoConstraints = false
88
+ avatar.isAccessibilityElement = false
89
+ let initial = UILabel()
90
+ initial.text = String(name.prefix(1)).uppercased()
91
+ initial.font = theme.uiFont(.secondary, size: 15, weight: .semibold)
92
+ initial.textColor = theme.uiColor(palette.avatarInk)
93
+ initial.translatesAutoresizingMaskIntoConstraints = false
94
+ avatar.addSubview(initial)
95
+ NSLayoutConstraint.activate([
96
+ initial.centerXAnchor.constraint(equalTo: avatar.centerXAnchor),
97
+ initial.centerYAnchor.constraint(equalTo: avatar.centerYAnchor),
98
+ avatar.widthAnchor.constraint(equalToConstant: 36),
99
+ avatar.heightAnchor.constraint(equalToConstant: 36),
100
+ ])
101
+ if let urlString = avatarURL {
102
+ Task { [weak avatar, weak initial] in
103
+ if let image = try? await ImageLoader.shared.load(urlString) {
104
+ await MainActor.run {
105
+ avatar?.image = image
106
+ initial?.isHidden = true
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ let title = UILabel()
113
+ title.text = name
114
+ title.font = theme.uiFont(.secondary, size: 14, weight: .semibold)
115
+ title.textColor = theme.uiColor(palette.ink)
116
+ title.adjustsFontForContentSizeCategory = true
117
+ let body = UILabel()
118
+ body.text = text
119
+ body.font = theme.uiFont(.secondary, size: 13.5)
120
+ body.textColor = theme.uiColor(palette.ink2)
121
+ body.numberOfLines = 2
122
+ body.adjustsFontForContentSizeCategory = true
123
+ let stack = UIStackView(arrangedSubviews: [title, body])
124
+ stack.axis = .vertical
125
+ stack.spacing = 2
126
+ let row = UIStackView(arrangedSubviews: [avatar, stack])
127
+ row.spacing = 12
128
+ row.alignment = .center
129
+ row.translatesAutoresizingMaskIntoConstraints = false
130
+ addSubview(row)
131
+ NSLayoutConstraint.activate([
132
+ row.topAnchor.constraint(equalTo: topAnchor, constant: 12),
133
+ row.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
134
+ row.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 14),
135
+ row.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -14),
136
+ ])
137
+ isAccessibilityElement = true
138
+ accessibilityLabel = "\(name). \(text)"
139
+ accessibilityTraits = .button
140
+ addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapped)))
141
+ let swipe = UISwipeGestureRecognizer(target: self, action: #selector(swiped))
142
+ swipe.direction = .up
143
+ addGestureRecognizer(swipe)
144
+ }
145
+
146
+ required init?(coder: NSCoder) { return nil }
147
+
148
+ @objc private func tapped() { onTap() }
149
+ @objc private func swiped() { onSwipeUp() }
150
+ override func accessibilityActivate() -> Bool {
151
+ onTap()
152
+ return true
153
+ }
154
+ }
155
+ #endif
@@ -0,0 +1,141 @@
1
+ #if canImport(SwiftUI) && canImport(UIKit)
2
+ import Combine
3
+ import Foundation
4
+ import UIKit
5
+
6
+ public enum ZellioChatLogLevel: Int, Sendable {
7
+ case none = 0
8
+ case error = 1
9
+ case info = 2
10
+ case debug = 3
11
+ }
12
+
13
+ public enum ZellioChatLocale: Sendable {
14
+ /// Follow the device, then the Studio language policy.
15
+ case auto
16
+ case arabic
17
+ case english
18
+
19
+ var core: ChatLocale? {
20
+ switch self {
21
+ case .auto: return nil
22
+ case .arabic: return .ar
23
+ case .english: return .en
24
+ }
25
+ }
26
+ }
27
+
28
+ public enum ZellioChatThemeMode: Sendable {
29
+ case auto
30
+ case light
31
+ case dark
32
+ }
33
+
34
+ /// Overrides for the look. A value set here wins over Studio.
35
+ public struct ZellioChatTheme {
36
+ public var mode: ZellioChatThemeMode
37
+ public var primaryColor: UIColor?
38
+ public var customerMessageColor: UIColor?
39
+ public var surfaceColor: UIColor?
40
+ /// The PostScript name of a font that the app has registered. Sizes still follow Dynamic Type.
41
+ public var fontName: String?
42
+
43
+ public init(mode: ZellioChatThemeMode = .auto, primaryColor: UIColor? = nil, customerMessageColor: UIColor? = nil,
44
+ surfaceColor: UIColor? = nil, fontName: String? = nil) {
45
+ self.mode = mode
46
+ self.primaryColor = primaryColor
47
+ self.customerMessageColor = customerMessageColor
48
+ self.surfaceColor = surfaceColor
49
+ self.fontName = fontName
50
+ }
51
+
52
+ var coreMode: ThemeMode? {
53
+ switch mode {
54
+ case .auto: return nil
55
+ case .light: return .light
56
+ case .dark: return .dark
57
+ }
58
+ }
59
+ }
60
+
61
+ public struct ZellioChatOptions {
62
+ /// Default: https://app.zellio.ai
63
+ public var baseURL: URL?
64
+ public var locale: ZellioChatLocale
65
+ public var theme: ZellioChatTheme
66
+ public var logLevel: ZellioChatLogLevel
67
+ /// The small banner for a new message while chat is closed and the app is open.
68
+ public var showsInAppBanner: Bool
69
+ /// false: the banner says "New message" and never shows the text.
70
+ public var bannerShowsPreview: Bool
71
+ /// Set by the React Native and Flutter wrappers: "react-native/1.0.0".
72
+ public var wrapper: String?
73
+ /// For demos and tests only: replaces the network. Leave nil in a real app.
74
+ public var transport: HTTPTransport?
75
+
76
+ public init(baseURL: URL? = nil, locale: ZellioChatLocale = .auto, theme: ZellioChatTheme = ZellioChatTheme(),
77
+ logLevel: ZellioChatLogLevel = .error, showsInAppBanner: Bool = true, bannerShowsPreview: Bool = true, wrapper: String? = nil, transport: HTTPTransport? = nil) {
78
+ self.baseURL = baseURL
79
+ self.locale = locale
80
+ self.theme = theme
81
+ self.logLevel = logLevel
82
+ self.showsInAppBanner = showsInAppBanner
83
+ self.bannerShowsPreview = bannerShowsPreview
84
+ self.transport = transport
85
+ self.wrapper = wrapper
86
+ }
87
+ }
88
+
89
+ /// The page or item the customer looks at. Sent with the next messages. Agents see it.
90
+ public struct ZellioChatContext: Equatable, Sendable {
91
+ public var screen: String?
92
+ public var itemType: String?
93
+ public var itemId: String?
94
+ public var itemTitle: String?
95
+ /// Must be https. Other values are dropped.
96
+ public var url: String?
97
+
98
+ public init(screen: String? = nil, itemType: String? = nil, itemId: String? = nil, itemTitle: String? = nil, url: String? = nil) {
99
+ self.screen = screen
100
+ self.itemType = itemType
101
+ self.itemId = itemId
102
+ self.itemTitle = itemTitle
103
+ self.url = url
104
+ }
105
+
106
+ var core: ChatContext { ChatContext(screen: screen, itemType: itemType, itemId: itemId, itemTitle: itemTitle, url: url) }
107
+ }
108
+
109
+ /// Events for the brand's own analytics. They never carry message text.
110
+ public enum ZellioChatEvent: Equatable, Sendable {
111
+ case chatOpened
112
+ case chatClosed
113
+ case messageSent
114
+ case messageReceived
115
+ case error(code: String)
116
+
117
+ public var name: String {
118
+ switch self {
119
+ case .chatOpened: return "chatOpened"
120
+ case .chatClosed: return "chatClosed"
121
+ case .messageSent: return "messageSent"
122
+ case .messageReceived: return "messageReceived"
123
+ case .error: return "error"
124
+ }
125
+ }
126
+
127
+ public var code: String? {
128
+ if case .error(let code) = self { return code }
129
+ return nil
130
+ }
131
+ }
132
+
133
+ /// Async source of the brand's identity JWT. Called when the SDK needs a session.
134
+ public typealias ZellioChatTokenProvider = @Sendable () async throws -> String
135
+
136
+ /// The unread number, for SwiftUI and UIKit.
137
+ public final class ZellioChatUnread: ObservableObject {
138
+ @Published public internal(set) var count: Int = 0
139
+ init() {}
140
+ }
141
+ #endif