@digia-engage/core 2.3.1 → 2.4.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/README.md +70 -0
  3. package/android/.project +28 -0
  4. package/android/build.gradle +1 -1
  5. package/android/local.properties +1 -0
  6. package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +64 -6
  7. package/ios/DigiaModule.swift +70 -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 +63 -2
  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 +65 -3
  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 +13 -14
  55. package/src/Digia.ts +142 -114
  56. package/src/DigiaAnchorView.tsx +6 -1
  57. package/src/DigiaProvider.tsx +76 -2
  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)
package/README.md CHANGED
@@ -266,6 +266,76 @@ react-native/ios/ iOS bridge
266
266
 
267
267
  ---
268
268
 
269
+ ## Build & publishing (hybrid model)
270
+
271
+ This package is published using the **hybrid** React Native library layout (the
272
+ [`react-native-builder-bob`](https://github.com/callstack/react-native-builder-bob)
273
+ convention): the npm tarball ships **both** the compiled output (`lib/`) **and**
274
+ the original TypeScript source (`src/`).
275
+
276
+ ### What the entry fields mean
277
+
278
+ | `package.json` field | Points to | Used by |
279
+ |---|---|---|
280
+ | `main` | `lib/commonjs/index` | Node / CommonJS consumers, Jest |
281
+ | `module` | `lib/module/index` | Bundlers that understand ESM |
282
+ | `types` | `lib/typescript/index.d.ts` | TypeScript |
283
+ | `react-native` / `source` | `src/index` | **Metro** — RN apps bundle straight from source |
284
+
285
+ Because Metro resolves the `react-native`/`source` field, a React Native app that
286
+ consumes this package bundles the **actual `.ts` source**. That gives our users
287
+ the best developer experience:
288
+
289
+ - **Stack traces** point at real `src/*.ts` lines, not transpiled output.
290
+ - **Step-debugging** walks through the real source.
291
+ - **Go-to-definition** lands on the real source — we ship `.d.ts` **and**
292
+ `.d.ts.map` (declaration maps) alongside `src/`, so an IDE jumps from the type
293
+ definition through to the `.ts` it came from.
294
+
295
+ The compiled `lib/` is a robust fallback for any tool that does *not* honour the
296
+ `react-native` field (Node, Jest, web bundlers, type resolvers), so the package
297
+ never breaks outside Metro.
298
+
299
+ ### Why not ship raw `src/` only?
300
+
301
+ Shipping only `src/*.ts` works in RN (Metro strips the types) but breaks
302
+ everywhere else — bundlers skip `node_modules` transpilation by default, and a
303
+ consumer's stricter `tsconfig` would re-type-check our source and surface errors
304
+ they can't fix. The hybrid layout keeps the great RN DX *and* stays safe for
305
+ every other consumer.
306
+
307
+ ### Build config that makes this work
308
+
309
+ `tsconfig.build.json` (used only to generate type definitions):
310
+
311
+ - `declaration: true` — emit `.d.ts`
312
+ - `declarationMap: true` — emit `.d.ts.map` so go-to-definition reaches `src/`
313
+ - `sourceMap: true` + `inlineSources: true` — map compiled JS back to source
314
+ - `rootDir: "src"` — keeps `index.d.ts` at the top of `lib/typescript/`
315
+ - **No** `declarationDir` / `noEmit` here — `bob` sets those via the CLI; leaving
316
+ them in the config produces conflict warnings
317
+
318
+ `files` includes both `src` and `lib` so the maps resolve on the consumer's disk.
319
+
320
+ ### Publishing
321
+
322
+ The `prepare` script runs `bob build` automatically on install and publish, so
323
+ the build toolchain (`typescript`, `react-native-builder-bob`) must be installed
324
+ first:
325
+
326
+ ```bash
327
+ npm install # installs devDeps AND runs prepare → bob build
328
+ npm pack --dry-run # verify lib/ + src/ + maps are in the tarball
329
+ npm publish
330
+ ```
331
+
332
+ > **Heads-up:** `package-lock.json` is **gitignored** for this library (a lib's
333
+ > lockfile is ignored by consumers and only hides dependency-range drift). On a
334
+ > fresh CI clone there is no lockfile, so use `npm install` — **not** `npm ci`,
335
+ > which requires one.
336
+
337
+ **Never hand-edit `lib/`** — it is generated. Edit `src/` and rebuild.
338
+
269
339
  ## License
270
340
 
271
341
  Business Source License 1.1 (BUSL-1.1) © Digia Technology Private Limited — see [LICENSE](LICENSE)
@@ -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.0'
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 -> {
@@ -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
 
@@ -140,10 +162,17 @@ final class DigiaModule: RCTEventEmitter {
140
162
  let content = buildInAppPayloadContent(from: contentMap)
141
163
  let cepContext = (cepContextMap as? [String: String]) ?? [:]
142
164
  let payload = InAppPayload(id: id, content: content, cepContext: cepContext)
165
+ print("[DigiaRN] triggerCampaign id=\(id) campaignKey=\(content.campaignKey ?? "nil")")
143
166
 
144
167
  Task { @MainActor in
145
- guard let delegate = self.rnPlugin.delegate else { return }
168
+ guard let delegate = self.rnPlugin.delegate else {
169
+ print("[DigiaRN] triggerCampaign: delegate is nil — registerBridge() may not have run yet")
170
+ return
171
+ }
146
172
  delegate.onCampaignTriggered(payload)
173
+ Task { @MainActor in
174
+ print("[DigiaRN] triggerCampaign post-call: hasActiveOverlay=\(Digia.hasActiveOverlay)")
175
+ }
147
176
  }
148
177
  }
149
178
 
@@ -213,12 +242,11 @@ final class DigiaModule: RCTEventEmitter {
213
242
  else { return }
214
243
 
215
244
  // Guard against double-mounting (e.g. fast-refresh).
216
- let mountTag = 0xD19140
217
- if rootVC.view.viewWithTag(mountTag) != nil { return }
245
+ if rootVC.view.viewWithTag(Self.overlayMountTag) != nil { return }
218
246
 
219
247
  let hc = UIHostingController(rootView: DigiaHostWrapperView())
220
248
  let hostView = DigiaRootOverlayView(hostingController: hc)
221
- hostView.tag = mountTag
249
+ hostView.tag = Self.overlayMountTag
222
250
  hostView.translatesAutoresizingMaskIntoConstraints = false
223
251
  hostView.backgroundColor = .clear
224
252
 
@@ -254,6 +282,7 @@ final class DigiaModule: RCTEventEmitter {
254
282
  let screenId = map["screenId"] as? String
255
283
  let campaignKey =
256
284
  (map["campaignKey"] as? String) ?? (map["campaign_key"] as? String)
285
+ ?? (map["digia_campaign_key"] as? String)
257
286
  ?? (map["digiaKey"] as? String)
258
287
  var type = (map["type"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
259
288
  if type.isEmpty {
@@ -266,6 +295,13 @@ final class DigiaModule: RCTEventEmitter {
266
295
  return raw.compactMapValues { JSONValue(rawValue: $0) }
267
296
  }()
268
297
 
298
+ // CEP trigger variables for `{{ }}` interpolation. CleverTap's nudge
299
+ // mapper puts them at `content.variables` (top-level); the command/inline
300
+ // mappers may nest them under `args.variables`. Mirror the JS bridge's
301
+ // `_extractVariables` (content.variables first, then args.variables).
302
+ let variables = Self.variableMap(map["variables"])
303
+ ?? Self.variableMap((map["args"] as? [String: Any])?["variables"])
304
+
269
305
  return InAppPayloadContent(
270
306
  type: type,
271
307
  placementKey: pk,
@@ -275,9 +311,35 @@ final class DigiaModule: RCTEventEmitter {
275
311
  command: command,
276
312
  args: args,
277
313
  screenId: screenId,
278
- campaignKey: campaignKey
314
+ campaignKey: campaignKey,
315
+ variables: variables
279
316
  )
280
317
  }
318
+
319
+ /// Coerces a raw `variables` value into a `[String: String]` map, stringifying
320
+ /// scalar values (string / number / bool) and dropping anything else. Returns
321
+ /// nil for a missing or empty map. Mirrors the JS `parseVariableMap`.
322
+ private static func variableMap(_ raw: Any?) -> [String: String]? {
323
+ guard let dict = raw as? [String: Any] else { return nil }
324
+ var result: [String: String] = [:]
325
+ for (key, value) in dict {
326
+ switch value {
327
+ case let string as String:
328
+ result[key] = string
329
+ case let number as NSNumber:
330
+ // Distinguish a boxed bool from a numeric NSNumber so `true`
331
+ // stays "true" rather than "1".
332
+ if CFGetTypeID(number) == CFBooleanGetTypeID() {
333
+ result[key] = number.boolValue ? "true" : "false"
334
+ } else {
335
+ result[key] = number.stringValue
336
+ }
337
+ default:
338
+ continue
339
+ }
340
+ }
341
+ return result.isEmpty ? nil : result
342
+ }
281
343
  }
282
344
 
283
345
  // 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