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.
Files changed (121) hide show
  1. package/CarrotQuest.podspec +34 -0
  2. package/LICENSE +20 -0
  3. package/README.md +457 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +136 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +11 -0
  8. package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuest.kt +585 -0
  9. package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuestPackage.kt +22 -0
  10. package/android/src/main/java/com/margelo/nitro/carrotquest/CarrotQuestSetup.kt +472 -0
  11. package/ios/CarrotQuest.swift +588 -0
  12. package/ios/CarrotQuestSetup.swift +333 -0
  13. package/lib/module/CarrotQuest.nitro.js +4 -0
  14. package/lib/module/CarrotQuest.nitro.js.map +1 -0
  15. package/lib/module/carrot-quest.js +136 -0
  16. package/lib/module/carrot-quest.js.map +1 -0
  17. package/lib/module/carrot-quest.native.js +155 -0
  18. package/lib/module/carrot-quest.native.js.map +1 -0
  19. package/lib/module/index.js +6 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/module/subscriptions.js +76 -0
  23. package/lib/module/subscriptions.js.map +1 -0
  24. package/lib/typescript/package.json +1 -0
  25. package/lib/typescript/src/CarrotQuest.nitro.d.ts +394 -0
  26. package/lib/typescript/src/CarrotQuest.nitro.d.ts.map +1 -0
  27. package/lib/typescript/src/carrot-quest.d.ts +37 -0
  28. package/lib/typescript/src/carrot-quest.d.ts.map +1 -0
  29. package/lib/typescript/src/carrot-quest.native.d.ts +37 -0
  30. package/lib/typescript/src/carrot-quest.native.d.ts.map +1 -0
  31. package/lib/typescript/src/index.d.ts +6 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -0
  33. package/lib/typescript/src/subscriptions.d.ts +32 -0
  34. package/lib/typescript/src/subscriptions.d.ts.map +1 -0
  35. package/nitro.json +23 -0
  36. package/nitrogen/generated/android/c++/JCarrotQuestEventValue.cpp +30 -0
  37. package/nitrogen/generated/android/c++/JCarrotQuestEventValue.hpp +83 -0
  38. package/nitrogen/generated/android/c++/JCarrotQuestLogCategory.hpp +76 -0
  39. package/nitrogen/generated/android/c++/JCarrotQuestLogEntry.hpp +100 -0
  40. package/nitrogen/generated/android/c++/JCarrotQuestLogLevel.hpp +70 -0
  41. package/nitrogen/generated/android/c++/JCarrotQuestProperty.hpp +74 -0
  42. package/nitrogen/generated/android/c++/JCarrotQuestPropertyKind.hpp +61 -0
  43. package/nitrogen/generated/android/c++/JCarrotQuestPropertyOperation.hpp +73 -0
  44. package/nitrogen/generated/android/c++/JCarrotQuestSetupOptions.hpp +94 -0
  45. package/nitrogen/generated/android/c++/JCarrotQuestTheme.hpp +64 -0
  46. package/nitrogen/generated/android/c++/JCarrotQuestUrlSource.hpp +64 -0
  47. package/nitrogen/generated/android/c++/JCarrotQuestUserProperties.hpp +84 -0
  48. package/nitrogen/generated/android/c++/JFunc_void_CarrotQuestLogEntry.hpp +84 -0
  49. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +75 -0
  50. package/nitrogen/generated/android/c++/JFunc_void_double.hpp +75 -0
  51. package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
  52. package/nitrogen/generated/android/c++/JHybridCarrotQuestSpec.cpp +388 -0
  53. package/nitrogen/generated/android/c++/JHybridCarrotQuestSpec.hpp +96 -0
  54. package/nitrogen/generated/android/carrotquest+autolinking.cmake +82 -0
  55. package/nitrogen/generated/android/carrotquest+autolinking.gradle +27 -0
  56. package/nitrogen/generated/android/carrotquestOnLoad.cpp +62 -0
  57. package/nitrogen/generated/android/carrotquestOnLoad.hpp +34 -0
  58. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestEventValue.kt +75 -0
  59. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogCategory.kt +29 -0
  60. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogEntry.kt +81 -0
  61. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestLogLevel.kt +27 -0
  62. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestProperty.kt +66 -0
  63. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestPropertyKind.kt +24 -0
  64. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestPropertyOperation.kt +28 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestSetupOptions.kt +91 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestTheme.kt +25 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestUrlSource.kt +25 -0
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/CarrotQuestUserProperties.kt +66 -0
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_CarrotQuestLogEntry.kt +80 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_bool.kt +80 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_double.kt +80 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/Func_void_std__string.kt +80 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/HybridCarrotQuestSpec.kt +209 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/carrotquest/carrotquestOnLoad.kt +35 -0
  75. package/nitrogen/generated/ios/CarrotQuest+autolinking.rb +62 -0
  76. package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Bridge.cpp +97 -0
  77. package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Bridge.hpp +637 -0
  78. package/nitrogen/generated/ios/CarrotQuest-Swift-Cxx-Umbrella.hpp +80 -0
  79. package/nitrogen/generated/ios/CarrotQuestAutolinking.mm +33 -0
  80. package/nitrogen/generated/ios/CarrotQuestAutolinking.swift +26 -0
  81. package/nitrogen/generated/ios/c++/HybridCarrotQuestSpecSwift.cpp +11 -0
  82. package/nitrogen/generated/ios/c++/HybridCarrotQuestSpecSwift.hpp +344 -0
  83. package/nitrogen/generated/ios/swift/CarrotQuestEventValue.swift +32 -0
  84. package/nitrogen/generated/ios/swift/CarrotQuestLogCategory.swift +64 -0
  85. package/nitrogen/generated/ios/swift/CarrotQuestLogEntry.swift +86 -0
  86. package/nitrogen/generated/ios/swift/CarrotQuestLogLevel.swift +56 -0
  87. package/nitrogen/generated/ios/swift/CarrotQuestProperty.swift +56 -0
  88. package/nitrogen/generated/ios/swift/CarrotQuestPropertyKind.swift +44 -0
  89. package/nitrogen/generated/ios/swift/CarrotQuestPropertyOperation.swift +60 -0
  90. package/nitrogen/generated/ios/swift/CarrotQuestSetupOptions.swift +172 -0
  91. package/nitrogen/generated/ios/swift/CarrotQuestTheme.swift +48 -0
  92. package/nitrogen/generated/ios/swift/CarrotQuestUrlSource.swift +48 -0
  93. package/nitrogen/generated/ios/swift/CarrotQuestUserProperties.swift +110 -0
  94. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  95. package/nitrogen/generated/ios/swift/Func_void_CarrotQuestLogEntry.swift +46 -0
  96. package/nitrogen/generated/ios/swift/Func_void_bool.swift +46 -0
  97. package/nitrogen/generated/ios/swift/Func_void_double.swift +46 -0
  98. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  99. package/nitrogen/generated/ios/swift/Func_void_std__optional_double_.swift +53 -0
  100. package/nitrogen/generated/ios/swift/Func_void_std__optional_std__string_.swift +53 -0
  101. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +46 -0
  102. package/nitrogen/generated/ios/swift/HybridCarrotQuestSpec.swift +88 -0
  103. package/nitrogen/generated/ios/swift/HybridCarrotQuestSpec_cxx.swift +674 -0
  104. package/nitrogen/generated/shared/c++/CarrotQuestLogCategory.hpp +100 -0
  105. package/nitrogen/generated/shared/c++/CarrotQuestLogEntry.hpp +114 -0
  106. package/nitrogen/generated/shared/c++/CarrotQuestLogLevel.hpp +92 -0
  107. package/nitrogen/generated/shared/c++/CarrotQuestProperty.hpp +101 -0
  108. package/nitrogen/generated/shared/c++/CarrotQuestPropertyKind.hpp +80 -0
  109. package/nitrogen/generated/shared/c++/CarrotQuestPropertyOperation.hpp +96 -0
  110. package/nitrogen/generated/shared/c++/CarrotQuestSetupOptions.hpp +121 -0
  111. package/nitrogen/generated/shared/c++/CarrotQuestTheme.hpp +84 -0
  112. package/nitrogen/generated/shared/c++/CarrotQuestUrlSource.hpp +84 -0
  113. package/nitrogen/generated/shared/c++/CarrotQuestUserProperties.hpp +97 -0
  114. package/nitrogen/generated/shared/c++/HybridCarrotQuestSpec.cpp +55 -0
  115. package/nitrogen/generated/shared/c++/HybridCarrotQuestSpec.hpp +118 -0
  116. package/package.json +140 -0
  117. package/src/CarrotQuest.nitro.ts +493 -0
  118. package/src/carrot-quest.native.tsx +241 -0
  119. package/src/carrot-quest.tsx +207 -0
  120. package/src/index.tsx +57 -0
  121. 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
+ }