@digia-engage/core 2.3.2 → 2.5.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 (63) hide show
  1. package/DigiaEngageReactNative.podspec +1 -1
  2. package/android/.project +28 -0
  3. package/android/build.gradle +1 -1
  4. package/android/local.properties +1 -0
  5. package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +64 -6
  6. package/ios/DigiaEngageModule.m +8 -0
  7. package/ios/DigiaModule.swift +117 -8
  8. package/ios/RNEventBridgePlugin.swift +2 -0
  9. package/lib/commonjs/Digia.js +139 -100
  10. package/lib/commonjs/Digia.js.map +1 -1
  11. package/lib/commonjs/DigiaAnchorView.js +11 -1
  12. package/lib/commonjs/DigiaAnchorView.js.map +1 -1
  13. package/lib/commonjs/DigiaProvider.js +50 -1
  14. package/lib/commonjs/DigiaProvider.js.map +1 -1
  15. package/lib/commonjs/NativeDigiaEngage.js +3 -0
  16. package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
  17. package/lib/commonjs/digiaAnchorRegistry.js +3 -1
  18. package/lib/commonjs/digiaAnchorRegistry.js.map +1 -1
  19. package/lib/commonjs/frequencyStore.js +3 -3
  20. package/lib/commonjs/frequencyStore.js.map +1 -1
  21. package/lib/commonjs/index.js +0 -7
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/module/Digia.js +139 -100
  24. package/lib/module/Digia.js.map +1 -1
  25. package/lib/module/DigiaAnchorView.js +11 -1
  26. package/lib/module/DigiaAnchorView.js.map +1 -1
  27. package/lib/module/DigiaProvider.js +50 -1
  28. package/lib/module/DigiaProvider.js.map +1 -1
  29. package/lib/module/NativeDigiaEngage.js +3 -0
  30. package/lib/module/NativeDigiaEngage.js.map +1 -1
  31. package/lib/module/digiaAnchorRegistry.js +3 -1
  32. package/lib/module/digiaAnchorRegistry.js.map +1 -1
  33. package/lib/module/frequencyStore.js +3 -3
  34. package/lib/module/frequencyStore.js.map +1 -1
  35. package/lib/module/index.js +4 -4
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/typescript/Digia.d.ts +17 -8
  38. package/lib/typescript/Digia.d.ts.map +1 -1
  39. package/lib/typescript/DigiaAnchorView.d.ts.map +1 -1
  40. package/lib/typescript/DigiaProvider.d.ts +3 -1
  41. package/lib/typescript/DigiaProvider.d.ts.map +1 -1
  42. package/lib/typescript/NativeDigiaEngage.d.ts +9 -0
  43. package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
  44. package/lib/typescript/digiaAnchorRegistry.d.ts +1 -0
  45. package/lib/typescript/digiaAnchorRegistry.d.ts.map +1 -1
  46. package/lib/typescript/frequencyStore.d.ts +1 -1
  47. package/lib/typescript/frequencyStore.d.ts.map +1 -1
  48. package/lib/typescript/index.d.ts +5 -5
  49. package/lib/typescript/index.d.ts.map +1 -1
  50. package/lib/typescript/templateTypes.d.ts +24 -1
  51. package/lib/typescript/templateTypes.d.ts.map +1 -1
  52. package/lib/typescript/types.d.ts +17 -13
  53. package/lib/typescript/types.d.ts.map +1 -1
  54. package/package.json +8 -9
  55. package/src/Digia.ts +142 -114
  56. package/src/DigiaAnchorView.tsx +6 -1
  57. package/src/DigiaProvider.tsx +56 -1
  58. package/src/NativeDigiaEngage.ts +20 -0
  59. package/src/digiaAnchorRegistry.ts +3 -1
  60. package/src/frequencyStore.ts +4 -4
  61. package/src/index.ts +5 -5
  62. package/src/templateTypes.ts +31 -1
  63. package/src/types.ts +17 -13
@@ -28,7 +28,7 @@ Pod::Spec.new do |s|
28
28
  s.dependency 'React-Core'
29
29
 
30
30
  # ── Digia Engage iOS SDK ──────────────────────────────────────────────────
31
- s.dependency 'DigiaEngage','~> 2.3.1'
31
+ s.dependency 'DigiaEngage','~> 2.4.0'
32
32
 
33
33
  # ── New Architecture (Fabric / TurboModules) support ─────────────────────
34
34
  install_modules_dependencies(s)
@@ -0,0 +1,28 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <projectDescription>
3
+ <name>digia-engage_core</name>
4
+ <comment>Project digia-engage_core created by Buildship.</comment>
5
+ <projects>
6
+ </projects>
7
+ <buildSpec>
8
+ <buildCommand>
9
+ <name>org.eclipse.buildship.core.gradleprojectbuilder</name>
10
+ <arguments>
11
+ </arguments>
12
+ </buildCommand>
13
+ </buildSpec>
14
+ <natures>
15
+ <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16
+ </natures>
17
+ <filteredResources>
18
+ <filter>
19
+ <id>1780322402778</id>
20
+ <name></name>
21
+ <type>30</type>
22
+ <matcher>
23
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
24
+ <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25
+ </matcher>
26
+ </filter>
27
+ </filteredResources>
28
+ </projectDescription>
@@ -71,7 +71,7 @@ android {
71
71
 
72
72
  dependencies {
73
73
  // Digia Engage Android library
74
- implementation 'tech.digia:engage:2.1.1'
74
+ implementation 'tech.digia:engage:2.2.1'
75
75
 
76
76
  // ── React Native ─────────────────────────────────────────────────────────
77
77
  // React Native is provided by the host app; mark as compileOnly so it is
@@ -0,0 +1 @@
1
+ sdk.dir=/Users/ram/Library/Android/sdk
@@ -29,6 +29,7 @@ import androidx.lifecycle.setViewTreeLifecycleOwner
29
29
  import androidx.lifecycle.setViewTreeViewModelStoreOwner
30
30
  import androidx.savedstate.SavedStateRegistryOwner
31
31
  import androidx.savedstate.setViewTreeSavedStateRegistryOwner
32
+ import com.digia.engage.CEPTriggerPayload
32
33
  import com.digia.engage.DiagnosticReport
33
34
  import com.digia.engage.Digia
34
35
  import com.digia.engage.DigiaCEPDelegate
@@ -126,6 +127,54 @@ internal class DigiaModule(
126
127
  Digia.setCurrentScreen(name)
127
128
  }
128
129
 
130
+ // ─── Analytics identity ───────────────────────────────────────────────────
131
+
132
+ @ReactMethod
133
+ fun setUserId(userId: String) {
134
+ Digia.setUserId(userId)
135
+ }
136
+
137
+ @ReactMethod
138
+ fun clearUserId() {
139
+ Digia.clearUserId()
140
+ }
141
+
142
+ // ─── Analytics event forwarding (guide / JS-rendered campaigns) ───────────
143
+
144
+ /**
145
+ * Records an analytics event originating from JS-rendered campaigns (guides).
146
+ * Native campaigns (nudge, inline, survey) are tracked internally by the SDK.
147
+ */
148
+ @ReactMethod
149
+ fun trackEvent(
150
+ eventType: String,
151
+ campaignId: String,
152
+ campaignKey: String,
153
+ campaignType: String,
154
+ elementId: String?,
155
+ ) {
156
+ android.util.Log.d("DigiaAnalytics", "[trackEvent] RN bridge received: type=$eventType campaignId=$campaignId campaignKey=$campaignKey campaignType=$campaignType elementId=$elementId")
157
+ val event = when (eventType) {
158
+ "impressed" -> DigiaExperienceEvent.Impressed
159
+ "clicked" -> DigiaExperienceEvent.Clicked(elementId)
160
+ "dismissed" -> DigiaExperienceEvent.Dismissed
161
+ else -> {
162
+ android.util.Log.w("DigiaAnalytics", "[trackEvent] unknown eventType='$eventType' — dropped")
163
+ return
164
+ }
165
+ }
166
+ val payload = InAppPayload(
167
+ id = campaignKey,
168
+ content = mapOf(
169
+ "campaign_id" to campaignId,
170
+ "campaign_key" to campaignKey,
171
+ "campaign_type" to campaignType,
172
+ ),
173
+ )
174
+ android.util.Log.d("DigiaAnalytics", "[trackEvent] forwarding to Digia.captureAnalyticsEvent: event=${event::class.simpleName}")
175
+ Digia.captureAnalyticsEvent(event, payload)
176
+ }
177
+
129
178
  // ─── triggerCampaign ──────────────────────────────────────────────────────
130
179
 
131
180
  /**
@@ -138,11 +187,20 @@ internal class DigiaModule(
138
187
  @ReactMethod
139
188
  fun triggerCampaign(id: String, content: ReadableMap, cepContext: ReadableMap) {
140
189
  val delegate = rnPlugin.delegate ?: return
190
+ val contentMap = content.toHashMap().toMap()
191
+ val campaignKey = (contentMap["digia_campaign_key"] as? String)
192
+ ?: (contentMap["campaign_key"] as? String)
193
+ ?: id
194
+ @Suppress("UNCHECKED_CAST")
195
+ val variables = (contentMap["variables"] as? Map<*, *>)
196
+ ?.entries
197
+ ?.associate { it.key.toString() to it.value?.toString().orEmpty() }
141
198
  delegate.onCampaignTriggered(
142
- InAppPayload(
143
- id = id,
144
- content = content.toHashMap().toMap(),
145
- cepContext = cepContext.toHashMap().toMap(),
199
+ CEPTriggerPayload(
200
+ cepCampaignId = id,
201
+ campaignKey = campaignKey,
202
+ cepMetadata = cepContext.toHashMap().toMap(),
203
+ variables = variables,
146
204
  )
147
205
  )
148
206
  }
@@ -277,10 +335,10 @@ internal class RNEventBridgePlugin(
277
335
  /* forwarded by Digia.setCurrentScreen() on the native side */
278
336
  }
279
337
 
280
- override fun notifyEvent(event: DigiaExperienceEvent, payload: InAppPayload) {
338
+ override fun notifyEvent(event: DigiaExperienceEvent, payload: CEPTriggerPayload) {
281
339
  val params =
282
340
  Arguments.createMap().apply {
283
- putString("campaignId", payload.id)
341
+ putString("campaignId", payload.cepCampaignId)
284
342
  when (event) {
285
343
  is DigiaExperienceEvent.Impressed -> putString("type", "impressed")
286
344
  is DigiaExperienceEvent.Clicked -> {
@@ -34,6 +34,14 @@ RCT_EXTERN_METHOD(registerBridge)
34
34
 
35
35
  RCT_EXTERN_METHOD(setCurrentScreen : (NSString *)name)
36
36
 
37
+ RCT_EXTERN_METHOD(setUserId : (NSString *)userId)
38
+
39
+ RCT_EXTERN_METHOD(clearUserId)
40
+
41
+ RCT_EXTERN_METHOD(trackEvent : (NSString *)eventType campaignId : (NSString *)
42
+ campaignId campaignKey : (NSString *)campaignKey campaignType
43
+ : (NSString *)campaignType elementId : (NSString *)elementId)
44
+
37
45
  RCT_EXTERN_METHOD(triggerCampaign : (NSString *)id content : (NSDictionary *)
38
46
  content cepContext : (NSDictionary *)cepContext)
39
47
 
@@ -78,13 +78,18 @@ final class DigiaModule: RCTEventEmitter {
78
78
  default: logLevelValue = .error
79
79
  }
80
80
 
81
+ let cleanBaseUrl = baseUrl.flatMap { url -> String? in
82
+ var s = url.trimmingCharacters(in: .whitespacesAndNewlines)
83
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/"))
84
+ if s.hasSuffix("/api/v1") { s = String(s.dropLast(7)) }
85
+ return s.isEmpty ? nil : s
86
+ }
87
+
81
88
  let config = DigiaConfig(
82
89
  apiKey: projectId,
83
90
  logLevel: logLevelValue,
84
91
  environment: envValue,
85
- developerConfig: baseUrl.flatMap {
86
- $0.isEmpty ? nil : DigiaDeveloperConfig(baseURL: $0)
87
- },
92
+ developerConfig: cleanBaseUrl.map { DigiaDeveloperConfig(baseURL: $0) },
88
93
  fontFamily: fontFamily.flatMap { $0.isEmpty ? nil : $0 }
89
94
  )
90
95
 
@@ -109,10 +114,27 @@ final class DigiaModule: RCTEventEmitter {
109
114
  @objc
110
115
  func registerBridge() {
111
116
  Task { @MainActor in
117
+ // Dismiss any stale overlay left over from the previous JS session.
118
+ Digia.dismissActiveNudge()
119
+
120
+ // After a JS reload, re-bring the overlay host to the front.
121
+ // Expo/RN may have added views during the reload cycle that sit on
122
+ // top of the host, causing the SwiftUI layer to be unreachable by
123
+ // touch even when hasActiveOverlay = true.
124
+ if let rootVC = UIApplication.shared.connectedScenes
125
+ .compactMap({ ($0 as? UIWindowScene)?.keyWindow?.rootViewController })
126
+ .first,
127
+ let hostView = rootVC.view.viewWithTag(Self.overlayMountTag) {
128
+ rootVC.view.bringSubviewToFront(hostView)
129
+ }
130
+
112
131
  Digia.register(self.rnPlugin)
132
+ print("[DigiaRN] registerBridge: rnPlugin registered, delegate=\(self.rnPlugin.delegate != nil ? "set" : "nil")")
113
133
  }
114
134
  }
115
135
 
136
+ private static let overlayMountTag = 0xD19140
137
+
116
138
  // ────────────────────────────────────────────────────────────────────────
117
139
  // MARK: - setCurrentScreen
118
140
 
@@ -123,6 +145,53 @@ final class DigiaModule: RCTEventEmitter {
123
145
  // Digia.setCurrentScreen(name)
124
146
  }
125
147
 
148
+ // ────────────────────────────────────────────────────────────────────────
149
+ // MARK: - Analytics identity
150
+
151
+ /// Sets the authenticated user ID for analytics identity stitching.
152
+ ///
153
+ /// Mirrors Android's DigiaModule.setUserId(). The public iOS SDK does not
154
+ /// expose Digia.setUserId yet, so this is a no-op placeholder (like
155
+ /// setCurrentScreen) that exists to satisfy the JS NativeDigiaEngage spec —
156
+ /// without it, `getModule()?.setUserId(...)` throws on iOS. Activate the body
157
+ /// once the iOS SDK adds the public API.
158
+ @objc
159
+ func setUserId(_ userId: String) {
160
+ // Digia.setUserId(userId)
161
+ }
162
+
163
+ /// Clears the user ID (e.g. on logout). No-op placeholder — see setUserId.
164
+ @objc
165
+ func clearUserId() {
166
+ // Digia.clearUserId()
167
+ }
168
+
169
+ // ────────────────────────────────────────────────────────────────────────
170
+ // MARK: - trackEvent (guide / JS-rendered campaigns)
171
+
172
+ /// Records an analytics event originating from JS-rendered campaigns
173
+ /// (guides / tooltips / spotlights). Native campaigns (nudge, inline,
174
+ /// survey) are tracked internally by the SDK.
175
+ ///
176
+ /// Mirrors Android's DigiaModule.trackEvent(), which forwards to
177
+ /// Digia.captureAnalyticsEvent(event, payload). The public iOS SDK does not
178
+ /// expose an equivalent capture API yet, so iOS analytics for JS-rendered
179
+ /// campaigns are not forwarded — this method logs the event and otherwise
180
+ /// no-ops. It MUST exist regardless: without it,
181
+ /// `getModule()?.trackEvent(...)` throws "trackEvent is not a function" and
182
+ /// crashes the JS render tree (DigiaProvider). Wire up the body once the iOS
183
+ /// SDK adds a public capture API.
184
+ @objc
185
+ func trackEvent(
186
+ _ eventType: String,
187
+ campaignId: String,
188
+ campaignKey: String,
189
+ campaignType: String,
190
+ elementId: String?
191
+ ) {
192
+ print("[DigiaRN] trackEvent type=\(eventType) campaignId=\(campaignId) campaignKey=\(campaignKey) campaignType=\(campaignType) elementId=\(elementId ?? "nil") — iOS capture API not available, dropping")
193
+ }
194
+
126
195
  // ────────────────────────────────────────────────────────────────────────
127
196
  // MARK: - triggerCampaign
128
197
 
@@ -140,10 +209,17 @@ final class DigiaModule: RCTEventEmitter {
140
209
  let content = buildInAppPayloadContent(from: contentMap)
141
210
  let cepContext = (cepContextMap as? [String: String]) ?? [:]
142
211
  let payload = InAppPayload(id: id, content: content, cepContext: cepContext)
212
+ print("[DigiaRN] triggerCampaign id=\(id) campaignKey=\(content.campaignKey ?? "nil")")
143
213
 
144
214
  Task { @MainActor in
145
- guard let delegate = self.rnPlugin.delegate else { return }
215
+ guard let delegate = self.rnPlugin.delegate else {
216
+ print("[DigiaRN] triggerCampaign: delegate is nil — registerBridge() may not have run yet")
217
+ return
218
+ }
146
219
  delegate.onCampaignTriggered(payload)
220
+ Task { @MainActor in
221
+ print("[DigiaRN] triggerCampaign post-call: hasActiveOverlay=\(Digia.hasActiveOverlay)")
222
+ }
147
223
  }
148
224
  }
149
225
 
@@ -213,12 +289,11 @@ final class DigiaModule: RCTEventEmitter {
213
289
  else { return }
214
290
 
215
291
  // Guard against double-mounting (e.g. fast-refresh).
216
- let mountTag = 0xD19140
217
- if rootVC.view.viewWithTag(mountTag) != nil { return }
292
+ if rootVC.view.viewWithTag(Self.overlayMountTag) != nil { return }
218
293
 
219
294
  let hc = UIHostingController(rootView: DigiaHostWrapperView())
220
295
  let hostView = DigiaRootOverlayView(hostingController: hc)
221
- hostView.tag = mountTag
296
+ hostView.tag = Self.overlayMountTag
222
297
  hostView.translatesAutoresizingMaskIntoConstraints = false
223
298
  hostView.backgroundColor = .clear
224
299
 
@@ -254,6 +329,7 @@ final class DigiaModule: RCTEventEmitter {
254
329
  let screenId = map["screenId"] as? String
255
330
  let campaignKey =
256
331
  (map["campaignKey"] as? String) ?? (map["campaign_key"] as? String)
332
+ ?? (map["digia_campaign_key"] as? String)
257
333
  ?? (map["digiaKey"] as? String)
258
334
  var type = (map["type"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
259
335
  if type.isEmpty {
@@ -266,6 +342,13 @@ final class DigiaModule: RCTEventEmitter {
266
342
  return raw.compactMapValues { JSONValue(rawValue: $0) }
267
343
  }()
268
344
 
345
+ // CEP trigger variables for `{{ }}` interpolation. CleverTap's nudge
346
+ // mapper puts them at `content.variables` (top-level); the command/inline
347
+ // mappers may nest them under `args.variables`. Mirror the JS bridge's
348
+ // `_extractVariables` (content.variables first, then args.variables).
349
+ let variables = Self.variableMap(map["variables"])
350
+ ?? Self.variableMap((map["args"] as? [String: Any])?["variables"])
351
+
269
352
  return InAppPayloadContent(
270
353
  type: type,
271
354
  placementKey: pk,
@@ -275,9 +358,35 @@ final class DigiaModule: RCTEventEmitter {
275
358
  command: command,
276
359
  args: args,
277
360
  screenId: screenId,
278
- campaignKey: campaignKey
361
+ campaignKey: campaignKey,
362
+ variables: variables
279
363
  )
280
364
  }
365
+
366
+ /// Coerces a raw `variables` value into a `[String: String]` map, stringifying
367
+ /// scalar values (string / number / bool) and dropping anything else. Returns
368
+ /// nil for a missing or empty map. Mirrors the JS `parseVariableMap`.
369
+ private static func variableMap(_ raw: Any?) -> [String: String]? {
370
+ guard let dict = raw as? [String: Any] else { return nil }
371
+ var result: [String: String] = [:]
372
+ for (key, value) in dict {
373
+ switch value {
374
+ case let string as String:
375
+ result[key] = string
376
+ case let number as NSNumber:
377
+ // Distinguish a boxed bool from a numeric NSNumber so `true`
378
+ // stays "true" rather than "1".
379
+ if CFGetTypeID(number) == CFBooleanGetTypeID() {
380
+ result[key] = number.boolValue ? "true" : "false"
381
+ } else {
382
+ result[key] = number.stringValue
383
+ }
384
+ default:
385
+ continue
386
+ }
387
+ }
388
+ return result.isEmpty ? nil : result
389
+ }
281
390
  }
282
391
 
283
392
  // MARK: - DigiaRootOverlayView
@@ -54,6 +54,8 @@ internal final class RNEventBridgePlugin: NSObject, DigiaCEPPlugin {
54
54
  case .dismissed:
55
55
  body["type"] = "dismissed"
56
56
  }
57
+ let hasEmitter = eventEmitter != nil
58
+ print("[DigiaRN] RNEventBridgePlugin.notifyEvent type=\(body["type"] ?? "?") campaignId=\(payload.id) hasEmitter=\(hasEmitter)")
57
59
  eventEmitter?.sendEvent(withName: "digiaEngageEvent", body: body)
58
60
  }
59
61