react-native-carrot-quest 0.1.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/CarrotQuest.podspec +34 -0
- package/LICENSE +20 -0
- package/README.md +457 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +136 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +11 -0
- package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuest.kt +585 -0
- package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuestPackage.kt +22 -0
- package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuestSetup.kt +472 -0
- package/ios/CarrotQuest.swift +588 -0
- package/ios/CarrotQuestSetup.swift +333 -0
- package/lib/module/CarrotQuest.nitro.js +4 -0
- package/lib/module/CarrotQuest.nitro.js.map +1 -0
- package/lib/module/carrot-quest.js +136 -0
- package/lib/module/carrot-quest.js.map +1 -0
- package/lib/module/carrot-quest.native.js +155 -0
- package/lib/module/carrot-quest.native.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/subscriptions.js +76 -0
- package/lib/module/subscriptions.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/CarrotQuest.nitro.d.ts +394 -0
- package/lib/typescript/src/CarrotQuest.nitro.d.ts.map +1 -0
- package/lib/typescript/src/carrot-quest.d.ts +37 -0
- package/lib/typescript/src/carrot-quest.d.ts.map +1 -0
- package/lib/typescript/src/carrot-quest.native.d.ts +37 -0
- package/lib/typescript/src/carrot-quest.native.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/subscriptions.d.ts +32 -0
- package/lib/typescript/src/subscriptions.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JCarrotQuestEventValue.cpp +30 -0
- package/nitrogen/generated/android/c++/JCarrotQuestEventValue.hpp +83 -0
- package/nitrogen/generated/android/c++/JCarrotQuestLogCategory.hpp +76 -0
- package/nitrogen/generated/android/c++/JCarrotQuestLogEntry.hpp +100 -0
- package/nitrogen/generated/android/c++/JCarrotQuestLogLevel.hpp +70 -0
- package/nitrogen/generated/android/c++/JCarrotQuestProperty.hpp +74 -0
- package/nitrogen/generated/android/c++/JCarrotQuestPropertyKind.hpp +61 -0
- package/nitrogen/generated/android/c++/JCarrotQuestPropertyOperation.hpp +73 -0
- package/nitrogen/generated/android/c++/JCarrotQuestSetupOptions.hpp +94 -0
- package/nitrogen/generated/android/c++/JCarrotQuestTheme.hpp +64 -0
- package/nitrogen/generated/android/c++/JCarrotQuestUrlSource.hpp +64 -0
- package/nitrogen/generated/android/c++/JCarrotQuestUserProperties.hpp +84 -0
- package/nitrogen/generated/android/c++/JFunc_void_CarrotQuestLogEntry.hpp +84 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridCarrotQuestSpec.cpp +388 -0
- package/nitrogen/generated/android/c++/JHybridCarrotQuestSpec.hpp +96 -0
- package/nitrogen/generated/android/carrotquest+autolinking.cmake +82 -0
- package/nitrogen/generated/android/carrotquest+autolinking.gradle +27 -0
- package/nitrogen/generated/android/carrotquestOnLoad.cpp +62 -0
- package/nitrogen/generated/android/carrotquestOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestEventValue.kt +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogCategory.kt +29 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogEntry.kt +81 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogLevel.kt +27 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestProperty.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestPropertyKind.kt +24 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestPropertyOperation.kt +28 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestSetupOptions.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestTheme.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestUrlSource.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestUserProperties.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_CarrotQuestLogEntry.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_double.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/HybridCarrotQuestSpec.kt +209 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/carrotquestOnLoad.kt +35 -0
- package/nitrogen/generated/ios/CarrotQuest+autolinking.rb +62 -0
- package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Bridge.cpp +97 -0
- package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Bridge.hpp +637 -0
- package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Umbrella.hpp +80 -0
- package/nitrogen/generated/ios/CarrotQuestAutolinking.mm +33 -0
- package/nitrogen/generated/ios/CarrotQuestAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridCarrotQuestSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridCarrotQuestSpecSwift.hpp +344 -0
- package/nitrogen/generated/ios/swift/CarrotQuestEventValue.swift +32 -0
- package/nitrogen/generated/ios/swift/CarrotQuestLogCategory.swift +64 -0
- package/nitrogen/generated/ios/swift/CarrotQuestLogEntry.swift +86 -0
- package/nitrogen/generated/ios/swift/CarrotQuestLogLevel.swift +56 -0
- package/nitrogen/generated/ios/swift/CarrotQuestProperty.swift +56 -0
- package/nitrogen/generated/ios/swift/CarrotQuestPropertyKind.swift +44 -0
- package/nitrogen/generated/ios/swift/CarrotQuestPropertyOperation.swift +60 -0
- package/nitrogen/generated/ios/swift/CarrotQuestSetupOptions.swift +172 -0
- package/nitrogen/generated/ios/swift/CarrotQuestTheme.swift +48 -0
- package/nitrogen/generated/ios/swift/CarrotQuestUrlSource.swift +48 -0
- package/nitrogen/generated/ios/swift/CarrotQuestUserProperties.swift +110 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_CarrotQuestLogEntry.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_double_.swift +53 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridCarrotQuestSpec.swift +88 -0
- package/nitrogen/generated/ios/swift/HybridCarrotQuestSpec_cxx.swift +674 -0
- package/nitrogen/generated/shared/c++/CarrotQuestLogCategory.hpp +100 -0
- package/nitrogen/generated/shared/c++/CarrotQuestLogEntry.hpp +114 -0
- package/nitrogen/generated/shared/c++/CarrotQuestLogLevel.hpp +92 -0
- package/nitrogen/generated/shared/c++/CarrotQuestProperty.hpp +101 -0
- package/nitrogen/generated/shared/c++/CarrotQuestPropertyKind.hpp +80 -0
- package/nitrogen/generated/shared/c++/CarrotQuestPropertyOperation.hpp +96 -0
- package/nitrogen/generated/shared/c++/CarrotQuestSetupOptions.hpp +121 -0
- package/nitrogen/generated/shared/c++/CarrotQuestTheme.hpp +84 -0
- package/nitrogen/generated/shared/c++/CarrotQuestUrlSource.hpp +84 -0
- package/nitrogen/generated/shared/c++/CarrotQuestUserProperties.hpp +97 -0
- package/nitrogen/generated/shared/c++/HybridCarrotQuestSpec.cpp +55 -0
- package/nitrogen/generated/shared/c++/HybridCarrotQuestSpec.hpp +118 -0
- package/package.json +140 -0
- package/src/CarrotQuest.nitro.ts +493 -0
- package/src/carrot-quest.native.tsx +241 -0
- package/src/carrot-quest.tsx +207 -0
- package/src/index.tsx +57 -0
- package/src/subscriptions.ts +83 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CarrotSDK
|
|
3
|
+
import NitroModules
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
/// Swift implementation of the `CarrotQuest` hybrid object.
|
|
7
|
+
///
|
|
8
|
+
/// Note the whole file talks to `CarrotSDK` directly — no Objective-C bridging
|
|
9
|
+
/// header, no `objc_msgSend`, no `NSClassFromString`.
|
|
10
|
+
final class CarrotQuest: HybridCarrotQuestSpec {
|
|
11
|
+
|
|
12
|
+
// MARK: - Lifecycle
|
|
13
|
+
|
|
14
|
+
var isConfigured: Bool {
|
|
15
|
+
return CarrotQuestSetup.isConfigured
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func setup(apiKey: String, options: CarrotQuestSetupOptions?) throws -> Promise<Void> {
|
|
19
|
+
let promise = Promise<Void>()
|
|
20
|
+
|
|
21
|
+
CarrotQuestSetup.configure(
|
|
22
|
+
apiKey: apiKey,
|
|
23
|
+
locale: options?.locale,
|
|
24
|
+
theme: options?.theme.map { NSNumber(value: Self.nativeTheme($0).rawValue) },
|
|
25
|
+
useEuServer: options?.useEuServer ?? false,
|
|
26
|
+
appGroup: options?.appGroup,
|
|
27
|
+
isServiceMode: options?.isServiceMode ?? false
|
|
28
|
+
) { error in
|
|
29
|
+
if let error = error {
|
|
30
|
+
promise.reject(withError: error)
|
|
31
|
+
} else {
|
|
32
|
+
promise.resolve()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return promise
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func getSdkVersion() throws -> String {
|
|
40
|
+
return Carrot.shared.version
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Keep setup readiness inside the native package, never in consuming apps.
|
|
44
|
+
private func whenConfigured<T>(
|
|
45
|
+
_ promise: Promise<T>,
|
|
46
|
+
operation: @escaping () -> Void
|
|
47
|
+
) {
|
|
48
|
+
CarrotQuestSetup.whenConfigured { error in
|
|
49
|
+
if let error = error {
|
|
50
|
+
promise.reject(withError: error)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
operation()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// MARK: - Authentication
|
|
59
|
+
|
|
60
|
+
func auth(userId: String, userAuthKey: String) throws -> Promise<String?> {
|
|
61
|
+
return authenticate { successHandler, errorHandler in
|
|
62
|
+
Carrot.shared.auth(
|
|
63
|
+
withUserId: userId,
|
|
64
|
+
withUserAuthKey: userAuthKey,
|
|
65
|
+
successHandler: successHandler,
|
|
66
|
+
errorHandler: errorHandler
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func hashedAuth(userId: String, hash: String) throws -> Promise<String?> {
|
|
72
|
+
return authenticate { successHandler, errorHandler in
|
|
73
|
+
Carrot.shared.hashedAuth(
|
|
74
|
+
withUserId: userId,
|
|
75
|
+
withHash: hash,
|
|
76
|
+
successHandler: successHandler,
|
|
77
|
+
errorHandler: errorHandler
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Shared plumbing for the two auth flows — they differ only in which SDK
|
|
83
|
+
/// call they make.
|
|
84
|
+
private func authenticate(
|
|
85
|
+
_ call: @escaping (@escaping (String) -> Void, @escaping (String) -> Void) -> Void
|
|
86
|
+
) -> Promise<String?> {
|
|
87
|
+
let promise = Promise<String?>()
|
|
88
|
+
|
|
89
|
+
whenConfigured(promise) {
|
|
90
|
+
call(
|
|
91
|
+
{ carrotId in promise.resolve(withResult: carrotId) },
|
|
92
|
+
{ message in promise.reject(withError: CarrotQuestError.authFailed(message)) }
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return promise
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func logout() throws -> Promise<Void> {
|
|
100
|
+
let promise = Promise<Void>()
|
|
101
|
+
|
|
102
|
+
whenConfigured(promise) {
|
|
103
|
+
Carrot.shared.logout(
|
|
104
|
+
successHandler: {
|
|
105
|
+
promise.resolve()
|
|
106
|
+
},
|
|
107
|
+
errorHandler: { message in
|
|
108
|
+
promise.reject(withError: CarrotQuestError.logoutFailed(message))
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return promise
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// MARK: - Chat UI
|
|
117
|
+
|
|
118
|
+
func openChat() throws -> Promise<Void> {
|
|
119
|
+
let promise = Promise<Void>()
|
|
120
|
+
|
|
121
|
+
whenConfigured(promise) {
|
|
122
|
+
DispatchQueue.main.async {
|
|
123
|
+
guard Self.hasActiveWindow() else {
|
|
124
|
+
promise.reject(withError: CarrotQuestError.noPresentingWindow)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Carrot.shared.openChat()
|
|
129
|
+
promise.resolve()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return promise
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func closeChat() throws -> Promise<Void> {
|
|
137
|
+
let promise = Promise<Void>()
|
|
138
|
+
|
|
139
|
+
whenConfigured(promise) {
|
|
140
|
+
DispatchQueue.main.async {
|
|
141
|
+
Carrot.shared.closeChat()
|
|
142
|
+
promise.resolve()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return promise
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
var isChatOpen: Bool {
|
|
150
|
+
return isConfigured && Carrot.shared.isOpen
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func onChatVisibilityChanged(listener: @escaping (Bool) -> Void) throws {
|
|
154
|
+
CarrotQuestSetup.runWhenConfigured("onChatVisibilityChanged") {
|
|
155
|
+
Carrot.shared.onVisibilityUIChanged { visible in
|
|
156
|
+
listener(visible)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
func setTheme(theme: CarrotQuestTheme) throws {
|
|
162
|
+
let native = Self.nativeTheme(theme)
|
|
163
|
+
|
|
164
|
+
CarrotQuestSetup.runWhenConfigured("setTheme") {
|
|
165
|
+
DispatchQueue.main.async {
|
|
166
|
+
Carrot.shared.setTheme(native)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private static func nativeTheme(_ theme: CarrotQuestTheme) -> Carrot.Theme {
|
|
172
|
+
switch theme {
|
|
173
|
+
case .light: return .light
|
|
174
|
+
case .dark: return .dark
|
|
175
|
+
// The SDK calls this `fromMobile`; Android calls the same concept
|
|
176
|
+
// `FROM_DEVICE`, which is the name the JS API uses.
|
|
177
|
+
case .fromdevice: return .fromMobile
|
|
178
|
+
case .fromweb: return .fromWeb
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private static func hasActiveWindow() -> Bool {
|
|
183
|
+
return UIApplication.shared.connectedScenes
|
|
184
|
+
.compactMap { $0 as? UIWindowScene }
|
|
185
|
+
.flatMap { $0.windows }
|
|
186
|
+
.contains { $0.isKeyWindow }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// MARK: - Unread state
|
|
190
|
+
|
|
191
|
+
func getUnreadConversationsCount() throws -> Promise<Double> {
|
|
192
|
+
let promise = Promise<Double>()
|
|
193
|
+
|
|
194
|
+
whenConfigured(promise) {
|
|
195
|
+
Carrot.shared.getUnreadConversationsCount { count in
|
|
196
|
+
promise.resolve(withResult: Double(count))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return promise
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func getUnreadMessagesCount() throws -> Promise<Double?> {
|
|
204
|
+
let promise = Promise<Double?>()
|
|
205
|
+
|
|
206
|
+
whenConfigured(promise) {
|
|
207
|
+
Carrot.shared.getUnreadMessagesCount { count in
|
|
208
|
+
promise.resolve(withResult: Double(count))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return promise
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func onUnreadConversationsChanged(listener: @escaping (Double) -> Void) throws {
|
|
216
|
+
// Android-only: the iOS SDK has no unread-conversations observer, only the
|
|
217
|
+
// one-shot getters above. Documented on the spec; intentionally inert here.
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// MARK: - User properties
|
|
221
|
+
|
|
222
|
+
func setUserProperties(properties: CarrotQuestUserProperties) throws {
|
|
223
|
+
let userProperties = Self.makeUserProperties(from: properties)
|
|
224
|
+
|
|
225
|
+
guard !userProperties.isEmpty else { return }
|
|
226
|
+
|
|
227
|
+
CarrotQuestSetup.runWhenConfigured("setUserProperties") {
|
|
228
|
+
Carrot.shared.setUserProperty(userProperties)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func setProperties(properties: [CarrotQuestProperty]) throws {
|
|
233
|
+
let userProperties = properties.compactMap(Self.makeProperty)
|
|
234
|
+
|
|
235
|
+
guard !userProperties.isEmpty else { return }
|
|
236
|
+
|
|
237
|
+
CarrotQuestSetup.runWhenConfigured("setProperties") {
|
|
238
|
+
Carrot.shared.setUserProperty(userProperties)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Map the ergonomic struct onto Carrot's property types.
|
|
243
|
+
///
|
|
244
|
+
/// Empty values are dropped rather than written as blank strings, and a
|
|
245
|
+
/// `custom` key that collides with a system property is ignored so the typed
|
|
246
|
+
/// field always wins.
|
|
247
|
+
private static func makeUserProperties(
|
|
248
|
+
from properties: CarrotQuestUserProperties
|
|
249
|
+
) -> [UserProperty] {
|
|
250
|
+
var result: [UserProperty] = []
|
|
251
|
+
|
|
252
|
+
let systemProperties: [(name: String, key: CarrotUserProperty.Property, value: String?)] = [
|
|
253
|
+
("name", .name, properties.name),
|
|
254
|
+
("phone", .phone, properties.phone),
|
|
255
|
+
("email", .email, properties.email),
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
let reservedKeys = Set(systemProperties.map { $0.name })
|
|
259
|
+
|
|
260
|
+
for property in systemProperties {
|
|
261
|
+
guard let value = property.value, !value.isEmpty else { continue }
|
|
262
|
+
|
|
263
|
+
result.append(CarrotUserProperty(key: property.key, value: value))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if let custom = properties.custom {
|
|
267
|
+
for (key, value) in custom {
|
|
268
|
+
guard !key.isEmpty, !value.isEmpty else { continue }
|
|
269
|
+
guard !reservedKeys.contains(key) else { continue }
|
|
270
|
+
|
|
271
|
+
result.append(UserProperty(key: key, value: value))
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return result
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private static func makeProperty(_ property: CarrotQuestProperty) -> UserProperty? {
|
|
279
|
+
guard !property.key.isEmpty, !property.value.isEmpty else { return nil }
|
|
280
|
+
|
|
281
|
+
// Nitro lower-cases enum cases when generating them, so this is
|
|
282
|
+
// `.updateorcreate`, not `.updateOrCreate`.
|
|
283
|
+
let operation = nativeOperation(property.operation ?? .updateorcreate)
|
|
284
|
+
|
|
285
|
+
switch property.kind ?? .custom {
|
|
286
|
+
case .system:
|
|
287
|
+
guard let key = systemKey(property.key) else {
|
|
288
|
+
// Unknown system key — fall back to a free-form property rather than
|
|
289
|
+
// silently dropping the value.
|
|
290
|
+
return UserProperty(key: property.key, value: property.value, operation: operation)
|
|
291
|
+
}
|
|
292
|
+
return CarrotUserProperty(key: key, value: property.value, operation: operation)
|
|
293
|
+
|
|
294
|
+
case .ecommerce:
|
|
295
|
+
guard let key = ecommerceKey(property.key) else {
|
|
296
|
+
return UserProperty(key: property.key, value: property.value, operation: operation)
|
|
297
|
+
}
|
|
298
|
+
return EcommerceUserProperty(key: key, value: property.value, operation: operation)
|
|
299
|
+
|
|
300
|
+
case .custom:
|
|
301
|
+
return UserProperty(key: property.key, value: property.value, operation: operation)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Map JS key names onto the SDK's enums explicitly.
|
|
306
|
+
///
|
|
307
|
+
/// Deliberately not `Property(rawValue:)` — the enums' wire values are not
|
|
308
|
+
/// part of the published interface and need not match the case names, so
|
|
309
|
+
/// round-tripping through `rawValue` would be guessing.
|
|
310
|
+
private static func systemKey(_ key: String) -> CarrotUserProperty.Property? {
|
|
311
|
+
switch key {
|
|
312
|
+
case "name": return .name
|
|
313
|
+
case "phone": return .phone
|
|
314
|
+
case "email": return .email
|
|
315
|
+
case "pushNotificationsSubscribe": return .pushNotificationsSubscribe
|
|
316
|
+
case "pushCampaignsSubscribe": return .pushCampaignsSubscribe
|
|
317
|
+
case "lastUtmSource": return .lastUtmSource
|
|
318
|
+
case "lastUtmMedium": return .lastUtmMedium
|
|
319
|
+
case "lastUtmCampaign": return .lastUtmCampaign
|
|
320
|
+
case "lastUtmTerm": return .lastUtmTerm
|
|
321
|
+
case "lastUtmContent": return .lastUtmContent
|
|
322
|
+
default: return nil
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private static func ecommerceKey(_ key: String) -> EcommerceUserProperty.Property? {
|
|
327
|
+
switch key {
|
|
328
|
+
case "cartAmount": return .cartAmount
|
|
329
|
+
case "viewedProducts": return .viewedProducts
|
|
330
|
+
case "cartItems": return .cartItems
|
|
331
|
+
case "lastOrderStatus": return .lastOrderStatus
|
|
332
|
+
case "lastPayment": return .lastPayment
|
|
333
|
+
case "revenue": return .revenue
|
|
334
|
+
case "profit": return .profit
|
|
335
|
+
case "group": return .group
|
|
336
|
+
case "discount": return .discount
|
|
337
|
+
case "ordersCount": return .ordersCount
|
|
338
|
+
case "orderedItems": return .orderedItems
|
|
339
|
+
case "orderedCategories": return .orderedCategories
|
|
340
|
+
case "viewedCategories": return .viewedCategories
|
|
341
|
+
default: return nil
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private static func nativeOperation(
|
|
346
|
+
_ operation: CarrotQuestPropertyOperation
|
|
347
|
+
) -> UserProperty.Operations {
|
|
348
|
+
switch operation {
|
|
349
|
+
case .updateorcreate: return .updateOrCreate
|
|
350
|
+
case .setonce: return .setOnce
|
|
351
|
+
case .add: return .add
|
|
352
|
+
case .delete: return .delete
|
|
353
|
+
case .append: return .append
|
|
354
|
+
case .union: return .union
|
|
355
|
+
case .exclude: return .exclude
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// MARK: - Tracking
|
|
360
|
+
|
|
361
|
+
func trackEvent(name: String, params: [String: CarrotQuestEventValue]?) throws {
|
|
362
|
+
guard !name.isEmpty else { return }
|
|
363
|
+
|
|
364
|
+
CarrotQuestSetup.runWhenConfigured("trackEvent") {
|
|
365
|
+
guard let params = params, !params.isEmpty else {
|
|
366
|
+
Carrot.shared.trackEvent(withName: name, withParams: "")
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
Carrot.shared.trackEvent(
|
|
371
|
+
withName: name,
|
|
372
|
+
withParamsDict: params.mapValues(Self.unwrap)
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// Unwrap the JS variant into the untyped value the SDK's params dictionary
|
|
378
|
+
/// expects, keeping the original JSON type.
|
|
379
|
+
private static func unwrap(_ value: CarrotQuestEventValue) -> Any {
|
|
380
|
+
switch value {
|
|
381
|
+
case .first(let bool): return bool
|
|
382
|
+
case .second(let string): return string
|
|
383
|
+
case .third(let number):
|
|
384
|
+
// JS has a single number type, so whole numbers arrive as Doubles.
|
|
385
|
+
// Send them as integers so `1` does not become `1.0` on the wire.
|
|
386
|
+
if number.isFinite, number == number.rounded(),
|
|
387
|
+
number >= Double(Int.min), number <= Double(Int.max) {
|
|
388
|
+
return Int(number)
|
|
389
|
+
}
|
|
390
|
+
return number
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
func trackScreen(name: String) throws {
|
|
395
|
+
guard !name.isEmpty else { return }
|
|
396
|
+
|
|
397
|
+
CarrotQuestSetup.runWhenConfigured("trackScreen") {
|
|
398
|
+
Carrot.shared.trackScreen(name)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
func trackUtm(url: String) throws {
|
|
403
|
+
guard let parsed = URL(string: url) else { return }
|
|
404
|
+
|
|
405
|
+
Carrot.shared.trackUtm(parsed)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// MARK: - Push notifications
|
|
409
|
+
|
|
410
|
+
func setPushToken(token: String) throws {
|
|
411
|
+
CarrotQuestSetup.runWhenConfigured("setPushToken") {
|
|
412
|
+
CarrotNotificationService.shared.setToken(token)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
func deletePushToken() throws {
|
|
417
|
+
CarrotQuestSetup.runWhenConfigured("deletePushToken") {
|
|
418
|
+
CarrotNotificationService.shared.deleteToken()
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
func pushNotificationsUnsubscribe() throws -> Promise<Void> {
|
|
423
|
+
let promise = Promise<Void>()
|
|
424
|
+
|
|
425
|
+
whenConfigured(promise) {
|
|
426
|
+
CarrotNotificationService.shared.pushNotificationsUnsubscribe()
|
|
427
|
+
promise.resolve()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return promise
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
func pushCampaignsUnsubscribe() throws -> Promise<Void> {
|
|
434
|
+
let promise = Promise<Void>()
|
|
435
|
+
|
|
436
|
+
whenConfigured(promise) {
|
|
437
|
+
CarrotNotificationService.shared.pushCampaignsUnsubscribe()
|
|
438
|
+
promise.resolve()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return promise
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
func isCarrotPush(payloadJson: String) throws -> Bool {
|
|
445
|
+
guard let userInfo = Self.decodePayload(payloadJson) else { return false }
|
|
446
|
+
|
|
447
|
+
return CarrotNotificationService.shared.canHandle(userInfo)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func isAutoMessage(payloadJson: String) throws -> Bool {
|
|
451
|
+
// The iOS SDK exposes no auto-message predicate; Android does. Carrot's
|
|
452
|
+
// own payloads carry the discriminator, so read it directly rather than
|
|
453
|
+
// reporting a value we cannot determine.
|
|
454
|
+
guard let userInfo = Self.decodePayload(payloadJson) else { return false }
|
|
455
|
+
|
|
456
|
+
if let type = userInfo["type"] as? String {
|
|
457
|
+
return type == "auto_message"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return false
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
func handlePushNotification(payloadJson: String) throws {
|
|
464
|
+
guard let userInfo = Self.decodePayload(payloadJson) else {
|
|
465
|
+
throw CarrotQuestError.invalidPayload
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
CarrotQuestSetup.runWhenConfigured("handlePushNotification") {
|
|
469
|
+
CarrotNotificationService.shared.show(from: userInfo, appGroudDomain: self.appGroup)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
func handlePushClick(payloadJson: String, openLink: Bool) throws {
|
|
474
|
+
guard let userInfo = Self.decodePayload(payloadJson) else {
|
|
475
|
+
throw CarrotQuestError.invalidPayload
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
CarrotQuestSetup.runWhenConfigured("handlePushClick") {
|
|
479
|
+
CarrotNotificationService.shared.clickNotification(
|
|
480
|
+
userInfo: userInfo,
|
|
481
|
+
appGroudDomain: self.appGroup,
|
|
482
|
+
openLink: openLink
|
|
483
|
+
)
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
func getPushLink(payloadJson: String) throws -> String? {
|
|
488
|
+
// The typed accessor takes a UNNotificationResponse, which JS cannot hold.
|
|
489
|
+
// Carrot's payloads carry the link under a stable key, so read it from the
|
|
490
|
+
// decoded dictionary instead.
|
|
491
|
+
guard let userInfo = Self.decodePayload(payloadJson) else { return nil }
|
|
492
|
+
|
|
493
|
+
for key in ["carrot_link", "link", "url"] {
|
|
494
|
+
if let value = userInfo[key] as? String, !value.isEmpty {
|
|
495
|
+
return value
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return nil
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
func wasPushShownEarlier(payloadJson: String) throws -> Promise<Bool> {
|
|
503
|
+
let promise = Promise<Bool>()
|
|
504
|
+
|
|
505
|
+
guard let userInfo = Self.decodePayload(payloadJson) else {
|
|
506
|
+
promise.reject(withError: CarrotQuestError.invalidPayload)
|
|
507
|
+
return promise
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
whenConfigured(promise) {
|
|
511
|
+
CarrotNotificationService.shared.isShownEarlier(
|
|
512
|
+
userInfo: userInfo,
|
|
513
|
+
appGroudDomain: self.appGroup
|
|
514
|
+
) { shown in
|
|
515
|
+
promise.resolve(withResult: shown)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return promise
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/// App Group recorded at setup, needed by the notification helpers to reach
|
|
523
|
+
/// the shared store written by the service extension.
|
|
524
|
+
private var appGroup: String? {
|
|
525
|
+
return CarrotQuestSetup.appGroup
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private static func decodePayload(_ json: String) -> [String: Any]? {
|
|
529
|
+
guard
|
|
530
|
+
let data = json.data(using: .utf8),
|
|
531
|
+
let object = try? JSONSerialization.jsonObject(with: data),
|
|
532
|
+
let dictionary = object as? [String: Any]
|
|
533
|
+
else {
|
|
534
|
+
return nil
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return dictionary
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// MARK: - Deep links
|
|
541
|
+
|
|
542
|
+
func onUrlOpen(source: CarrotQuestUrlSource, listener: @escaping (String) -> Void) throws {
|
|
543
|
+
CustomUrlOpener.shared.set(for: Self.nativeUrlType(source)) { url in
|
|
544
|
+
listener(url.absoluteString)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private static func nativeUrlType(_ source: CarrotQuestUrlSource) -> CustomUrlOpener.UrlType {
|
|
549
|
+
switch source {
|
|
550
|
+
case .push: return .push
|
|
551
|
+
case .chat: return .chat
|
|
552
|
+
case .popup: return .popup
|
|
553
|
+
case .all: return .all
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
func openBrowserLink(url: String) throws {
|
|
558
|
+
guard let parsed = URL(string: url) else { return }
|
|
559
|
+
|
|
560
|
+
DispatchQueue.main.async {
|
|
561
|
+
CustomUrlOpener.shared.openBrowserLink(parsed)
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
func openUniversalLink(url: String) throws {
|
|
566
|
+
guard let parsed = URL(string: url) else { return }
|
|
567
|
+
|
|
568
|
+
DispatchQueue.main.async {
|
|
569
|
+
CustomUrlOpener.shared.openUniversalLink(parsed)
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// MARK: - Diagnostics
|
|
574
|
+
|
|
575
|
+
func getDiagnostics() throws -> String? {
|
|
576
|
+
// Android-only: the iOS SDK exposes no diagnostics dump.
|
|
577
|
+
return nil
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
func onLog(listener: @escaping (CarrotQuestLogEntry) -> Void) throws {
|
|
581
|
+
// Android-only: the iOS SDK has no logging hook to attach to.
|
|
582
|
+
// Documented on the spec; intentionally inert here.
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
func offLog() throws {
|
|
586
|
+
// Nothing was ever attached on iOS.
|
|
587
|
+
}
|
|
588
|
+
}
|