@digia-engage/core 2.0.1 → 2.0.2

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 (55) hide show
  1. package/README.md +38 -5
  2. package/android/build.gradle +1 -0
  3. package/android/local.properties +1 -1
  4. package/android/src/main/java/com/digia/engage/rn/DigiaAnchorViewManager.kt +67 -43
  5. package/ios/DigiaAnchorViewManager.swift +51 -27
  6. package/ios/DigiaEngageModule.m +7 -11
  7. package/ios/DigiaModule.swift +57 -90
  8. package/lib/commonjs/Digia.js +14 -2
  9. package/lib/commonjs/Digia.js.map +1 -1
  10. package/lib/commonjs/DigiaAnchorView.js +47 -41
  11. package/lib/commonjs/DigiaAnchorView.js.map +1 -1
  12. package/lib/commonjs/DigiaProvider.js +0 -2
  13. package/lib/commonjs/DigiaProvider.js.map +1 -1
  14. package/lib/commonjs/NativeDigiaEngage.js +0 -6
  15. package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
  16. package/lib/commonjs/actionHandler.js +11 -0
  17. package/lib/commonjs/actionHandler.js.map +1 -1
  18. package/lib/commonjs/index.js +26 -0
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/module/Digia.js +14 -2
  21. package/lib/module/Digia.js.map +1 -1
  22. package/lib/module/DigiaAnchorView.js +47 -42
  23. package/lib/module/DigiaAnchorView.js.map +1 -1
  24. package/lib/module/DigiaProvider.js +0 -2
  25. package/lib/module/DigiaProvider.js.map +1 -1
  26. package/lib/module/NativeDigiaEngage.js +0 -6
  27. package/lib/module/NativeDigiaEngage.js.map +1 -1
  28. package/lib/module/actionHandler.js +11 -0
  29. package/lib/module/actionHandler.js.map +1 -1
  30. package/lib/module/index.js +2 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/typescript/Digia.d.ts +1 -0
  33. package/lib/typescript/Digia.d.ts.map +1 -1
  34. package/lib/typescript/DigiaAnchorView.d.ts +4 -20
  35. package/lib/typescript/DigiaAnchorView.d.ts.map +1 -1
  36. package/lib/typescript/DigiaProvider.d.ts.map +1 -1
  37. package/lib/typescript/NativeDigiaEngage.d.ts +7 -4
  38. package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
  39. package/lib/typescript/actionHandler.d.ts +1 -0
  40. package/lib/typescript/actionHandler.d.ts.map +1 -1
  41. package/lib/typescript/index.d.ts +4 -1
  42. package/lib/typescript/index.d.ts.map +1 -1
  43. package/lib/typescript/templateTypes.d.ts +6 -0
  44. package/lib/typescript/templateTypes.d.ts.map +1 -1
  45. package/lib/typescript/types.d.ts +4 -0
  46. package/lib/typescript/types.d.ts.map +1 -1
  47. package/package.json +2 -2
  48. package/src/Digia.ts +17 -1
  49. package/src/DigiaAnchorView.tsx +36 -39
  50. package/src/DigiaProvider.tsx +0 -2
  51. package/src/NativeDigiaEngage.ts +4 -14
  52. package/src/actionHandler.ts +6 -0
  53. package/src/index.ts +13 -1
  54. package/src/templateTypes.ts +7 -7
  55. package/src/types.ts +2 -1
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # @digia-engage/core
2
2
 
3
- React Native SDK for **Digia Engage** renders native Android (Jetpack Compose) and iOS campaign UI (bottom sheets, dialogs, inline banners, tooltips, spotlights) inside React Native applications.
3
+ React Native bridge for the **Digia Engage SDK** renders in-app experiences
4
+ (tooltips, spotlights, carousels, surveys) inside React Native applications.
4
5
 
5
6
  > **Platform support**
6
7
  > | Platform | Status |
@@ -8,11 +9,42 @@ React Native SDK for **Digia Engage** – renders native Android (Jetpack Compos
8
9
  > | Android | ✅ Full support |
9
10
  > | iOS | ✅ Guide overlays (JS renderer); native bridge (surveys, inline) |
10
11
 
12
+ ---
13
+
14
+ ## How it works
15
+
16
+ Guide campaigns (tooltip / spotlight) are rendered entirely in **JavaScript** by
17
+ `DigiaProvider.tsx` using `@floating-ui/core` for anchor positioning. Surveys
18
+ and inline carousels are forwarded to the **native Android/iOS SDK** via the
19
+ bridge for Compose/SwiftUI rendering.
20
+
21
+ ```
22
+ CEP plugin (e.g. CleverTap)
23
+
24
+
25
+ Digia.onCampaignTriggered(payload)
26
+
27
+ ├─ campaign_type === 'guide' → DigiaGuideController → DigiaProvider.tsx (JS)
28
+ │ TooltipOverlay / SpotlightOverlay
29
+
30
+ ├─ campaign_type === 'inline' → nativeDigiaModule.triggerCampaign()
31
+ │ Android: DigiaSlot composable → VWCarousel
32
+
33
+ └─ campaign_type === 'survey' → nativeDigiaModule.triggerCampaign()
34
+ Android: SurveyRenderer composable
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
11
41
  ```sh
12
42
  npm install @digia-engage/core
43
+ # or
44
+ yarn add @digia-engage/core
13
45
  ```
14
46
 
15
- React Native CLI auto-linking handles the rest. Rebuild the native app after installing:
47
+ React Native CLI auto-linking handles the rest. Rebuild the native app:
16
48
 
17
49
  ```sh
18
50
  npx react-native run-android
@@ -50,7 +82,8 @@ await Digia.initialize({
50
82
 
51
83
  ### 2 — Mount `<DigiaHost />`
52
84
 
53
- Place `<DigiaHost />` at the root of your component tree. It renders the JS-side guide/tooltip/spotlight overlays via a `Modal`.
85
+ Place `<DigiaHost />` at the root of your component tree. It renders the
86
+ JS-side guide/tooltip/spotlight overlays via a `Modal`.
54
87
 
55
88
  ```tsx
56
89
  // app/_layout.tsx (Expo Router) or App.tsx
@@ -211,8 +244,8 @@ react-native/src/
211
244
  DigiaSlotView.tsx Native slot view wrapper; auto-sizes to content height
212
245
  DigiaHostView.tsx Low-level native overlay host (transparent, pointer-events none)
213
246
  NativeDigiaEngage.ts Codegen native module spec (TurboModule)
214
- actionHandler.ts Action execution — deep link, open URL, next/prev/dismiss;
215
- onAction override; cold-start queue
247
+ actionHandler.ts Action execution — deep link, open URL, next/prev/dismiss,
248
+ fire_event; onAction override; cold-start queue
216
249
  defaultInAppBrowser.ts Lazily loads react-native-inappbrowser-reborn
217
250
  templateTypes.ts TypeScript types for TooltipConfig, SpotlightConfig,
218
251
  CarouselConfig, SurveyTemplateConfig
@@ -70,6 +70,7 @@ android {
70
70
  }
71
71
 
72
72
  dependencies {
73
+ // Digia Engage Android library
73
74
  implementation 'tech.digia:engage:2.0.0'
74
75
 
75
76
  // ── React Native ─────────────────────────────────────────────────────────
@@ -1 +1 @@
1
- sdk.dir=/Users/adityachoubey/Library/Android/sdk
1
+ sdk.dir=/Users/ram/Library/Android/sdk
@@ -1,65 +1,89 @@
1
1
  package com.digia.engage.rn
2
2
 
3
- import android.graphics.Color
4
- import android.graphics.Outline
3
+ import android.content.Context
5
4
  import android.view.View
6
- import android.view.ViewOutlineProvider
5
+ import android.view.ViewTreeObserver
7
6
  import android.widget.FrameLayout
8
- import androidx.lifecycle.LifecycleOwner
9
- import androidx.lifecycle.ViewModelStoreOwner
10
- import androidx.lifecycle.setViewTreeLifecycleOwner
11
- import androidx.lifecycle.setViewTreeViewModelStoreOwner
12
- import androidx.savedstate.SavedStateRegistryOwner
13
- import androidx.savedstate.setViewTreeSavedStateRegistryOwner
14
- import com.digia.engage.DigiaAnchorView
7
+ import com.digia.engage.Digia
15
8
  import com.facebook.react.uimanager.ThemedReactContext
16
9
  import com.facebook.react.uimanager.ViewGroupManager
17
10
  import com.facebook.react.uimanager.annotations.ReactProp
18
11
 
19
- internal class DigiaAnchorViewManager : ViewGroupManager<DigiaAnchorView>() {
12
+ internal class DigiaAnchorContainerView(context: Context) : FrameLayout(context) {
20
13
 
21
- override fun getName(): String = VIEW_NAME
14
+ var anchorKey: String = ""
22
15
 
23
- override fun createViewInstance(context: ThemedReactContext): DigiaAnchorView {
24
- val activityContext = context.currentActivity ?: context
25
- val view = DigiaAnchorView(activityContext)
16
+ private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener { reportPosition() }
26
17
 
27
- val activity = context.currentActivity
28
- if (activity is LifecycleOwner) view.setViewTreeLifecycleOwner(activity)
29
- if (activity is ViewModelStoreOwner) view.setViewTreeViewModelStoreOwner(activity)
30
- if (activity is SavedStateRegistryOwner) view.setViewTreeSavedStateRegistryOwner(activity)
18
+ override fun onAttachedToWindow() {
19
+ super.onAttachedToWindow()
20
+ viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
21
+ reportPosition()
22
+ }
31
23
 
32
- view.layoutParams = FrameLayout.LayoutParams(
33
- FrameLayout.LayoutParams.WRAP_CONTENT,
34
- FrameLayout.LayoutParams.WRAP_CONTENT,
35
- )
36
- // Prevent the FrameLayout background from leaking white in spotlight corners.
37
- view.setBackgroundColor(Color.TRANSPARENT)
38
- return view
24
+ override fun onDetachedFromWindow() {
25
+ viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
26
+ if (anchorKey.isNotBlank()) Digia.unregisterAnchor(anchorKey)
27
+ super.onDetachedFromWindow()
39
28
  }
40
29
 
41
- @ReactProp(name = "anchorKey")
42
- fun setAnchorKey(view: DigiaAnchorView, anchorKey: String?) {
43
- view.anchorKey = anchorKey.orEmpty()
30
+ private fun reportPosition() {
31
+ val key = anchorKey.takeIf { it.isNotBlank() } ?: return
32
+ // React Native (Fabric) may not call layout() on this FrameLayout wrapper,
33
+ // so this.width/height can be 0. Derive dimensions from the first child instead.
34
+ val targetView: android.view.View = if (childCount > 0) getChildAt(0) else this
35
+ val w = targetView.width
36
+ val h = targetView.height
37
+ if (w == 0 || h == 0) return // not measured yet — wait for next layout pass
38
+ val loc = IntArray(2)
39
+ targetView.getLocationOnScreen(loc)
40
+ android.util.Log.d(
41
+ "Digia",
42
+ "[DigiaAnchorView] registerAnchor key='$key' x=${loc[0]} y=${loc[1]} w=$w h=$h"
43
+ )
44
+ Digia.registerAnchor(key, loc[0], loc[1], w, h)
45
+ Digia.registerAnchorView(key, targetView)
44
46
  }
47
+ }
48
+
49
+ // ViewGroupManager (not SimpleViewManager) because Fabric calls getViewGroupManager()
50
+ // on any view that hosts RN children — DigiaAnchorView wraps the anchor target element.
51
+ internal class DigiaAnchorViewManager : ViewGroupManager<DigiaAnchorContainerView>() {
52
+
53
+ override fun getName(): String = VIEW_NAME
54
+
55
+ override fun createViewInstance(context: ThemedReactContext) = DigiaAnchorContainerView(context)
45
56
 
46
- @ReactProp(name = "cornerRadius", defaultFloat = 0f)
47
- fun setCornerRadius(view: DigiaAnchorView, cornerRadius: Float) {
48
- val px = cornerRadius * view.resources.displayMetrics.density
49
- view.spotlightCornerRadius = px
50
- if (px > 0f) {
51
- view.outlineProvider = object : ViewOutlineProvider() {
52
- override fun getOutline(v: View, outline: Outline) {
53
- outline.setRoundRect(0, 0, v.width, v.height, px)
54
- }
55
- }
56
- view.clipToOutline = true
57
- } else {
58
- view.clipToOutline = false
59
- view.outlineProvider = ViewOutlineProvider.BOUNDS
57
+ // Use updateProperties instead of @ReactProp to be compatible with both
58
+ // Old Architecture (Paper) and New Architecture (Fabric) without codegen.
59
+ override fun updateProperties(
60
+ viewToUpdate: DigiaAnchorContainerView,
61
+ props: com.facebook.react.uimanager.ReactStylesDiffMap,
62
+ ) {
63
+ super.updateProperties(viewToUpdate, props)
64
+ if (props.hasKey("anchorKey")) {
65
+ viewToUpdate.anchorKey = props.getString("anchorKey") ?: ""
60
66
  }
61
67
  }
62
68
 
69
+ @com.facebook.react.uimanager.annotations.ReactProp(name = "anchorKey")
70
+ fun setAnchorKey(view: DigiaAnchorContainerView, key: String?) {
71
+ view.anchorKey = key.orEmpty()
72
+ }
73
+
74
+ override fun addView(parent: DigiaAnchorContainerView, child: View, index: Int) {
75
+ parent.addView(child, index)
76
+ }
77
+
78
+ override fun getChildCount(parent: DigiaAnchorContainerView): Int = parent.childCount
79
+
80
+ override fun getChildAt(parent: DigiaAnchorContainerView, index: Int): View =
81
+ parent.getChildAt(index)
82
+
83
+ override fun removeViewAt(parent: DigiaAnchorContainerView, index: Int) {
84
+ parent.removeViewAt(index)
85
+ }
86
+
63
87
  companion object {
64
88
  const val VIEW_NAME = "DigiaAnchorView"
65
89
  }
@@ -1,43 +1,67 @@
1
+ /**
2
+ * DigiaAnchorViewManager
3
+ *
4
+ * iOS RCTViewManager that vends DigiaAnchorContainerUIView — a UIView wrapper
5
+ * that automatically tracks its screen-coordinate frame and registers it with
6
+ * AnchorRegistry whenever it lays out or moves to a new window.
7
+ *
8
+ * This mirrors Android's DigiaAnchorViewManager / DigiaAnchorContainerView.
9
+ */
10
+
11
+ import Foundation
1
12
  import React
2
13
  import UIKit
3
14
  import DigiaEngage
4
15
 
5
- @objc(DigiaAnchorView)
6
- final class DigiaAnchorViewManager: RCTViewManager {
7
-
8
- override static func requiresMainQueueSetup() -> Bool { true }
9
-
10
- override func view() -> UIView! {
11
- return DigiaAnchorUIView()
12
- }
13
-
14
- @objc func setAnchorKey(_ anchorKey: String, forView view: DigiaAnchorUIView) {
15
- view.anchorKey = anchorKey
16
- }
17
- }
16
+ // MARK: - DigiaAnchorContainerUIView
18
17
 
19
- // MARK: - DigiaAnchorUIView
18
+ @objc(DigiaAnchorContainerUIView)
19
+ final class DigiaAnchorContainerUIView: UIView {
20
20
 
21
- final class DigiaAnchorUIView: UIView {
22
-
23
- var anchorKey: String = "" {
21
+ @objc var anchorKey: String = "" {
24
22
  didSet {
25
- guard anchorKey != oldValue else { return }
26
- if !oldValue.isEmpty {
27
- AnchorRegistry.shared.unregister(key: oldValue)
28
- }
29
- if !anchorKey.isEmpty, window != nil {
30
- AnchorRegistry.shared.register(key: anchorKey, view: self)
23
+ if !oldValue.isEmpty && oldValue != anchorKey {
24
+ Task { @MainActor in AnchorRegistry.shared.unregister(key: oldValue) }
31
25
  }
26
+ reportPosition()
32
27
  }
33
28
  }
34
29
 
30
+ override func layoutSubviews() {
31
+ super.layoutSubviews()
32
+ reportPosition()
33
+ }
34
+
35
35
  override func didMoveToWindow() {
36
36
  super.didMoveToWindow()
37
- if window != nil, !anchorKey.isEmpty {
38
- AnchorRegistry.shared.register(key: anchorKey, view: self)
39
- } else if window == nil, !anchorKey.isEmpty {
40
- AnchorRegistry.shared.unregister(key: anchorKey)
37
+ if window == nil {
38
+ guard !anchorKey.isEmpty else { return }
39
+ let key = anchorKey
40
+ Task { @MainActor in AnchorRegistry.shared.unregister(key: key) }
41
+ } else {
42
+ reportPosition()
43
+ }
44
+ }
45
+
46
+ private func reportPosition() {
47
+ guard !anchorKey.isEmpty, window != nil else { return }
48
+ // convert(bounds, to: nil) gives frame in window coordinates (UIKit points)
49
+ let rectInWindow = convert(bounds, to: nil)
50
+ let key = anchorKey
51
+ Task { @MainActor in
52
+ AnchorRegistry.shared.register(key: key, rect: rectInWindow)
41
53
  }
42
54
  }
43
55
  }
56
+
57
+ // MARK: - DigiaAnchorViewManager
58
+
59
+ @objc(DigiaAnchorView)
60
+ final class DigiaAnchorViewManager: RCTViewManager {
61
+
62
+ override static func requiresMainQueueSetup() -> Bool { true }
63
+
64
+ override func view() -> UIView! {
65
+ DigiaAnchorContainerUIView()
66
+ }
67
+ }
@@ -40,13 +40,13 @@ RCT_EXTERN_METHOD(triggerCampaign:(NSString *)id
40
40
 
41
41
  RCT_EXTERN_METHOD(invalidateCampaign:(NSString *)campaignId)
42
42
 
43
- RCT_EXTERN_METHOD(showAnchoredOverlay:(NSString *)id
44
- content:(NSDictionary *)content
45
- cepContext:(NSDictionary *)cepContext
46
- anchorX:(double)anchorX
47
- anchorY:(double)anchorY
48
- anchorWidth:(double)anchorWidth
49
- anchorHeight:(double)anchorHeight)
43
+ RCT_EXTERN_METHOD(registerAnchor:(NSString *)key
44
+ x:(double)x
45
+ y:(double)y
46
+ width:(double)width
47
+ height:(double)height)
48
+
49
+ RCT_EXTERN_METHOD(unregisterAnchor:(NSString *)key)
50
50
 
51
51
  @end
52
52
 
@@ -64,7 +64,3 @@ RCT_EXPORT_VIEW_PROPERTY(onContentSizeChange, RCTDirectEventBlock)
64
64
  @interface RCT_EXTERN_MODULE(DigiaAnchorView, RCTViewManager)
65
65
  RCT_EXPORT_VIEW_PROPERTY(anchorKey, NSString)
66
66
  @end
67
-
68
- @interface RCT_EXTERN_MODULE(DigiaAnchorView, RCTViewManager)
69
- RCT_EXPORT_VIEW_PROPERTY(anchorKey, NSString)
70
- @end
@@ -1,3 +1,4 @@
1
+ import DigiaEngage
1
2
  /**
2
3
  * DigiaModule
3
4
  *
@@ -30,7 +31,6 @@ import Foundation
30
31
  import React
31
32
  import SwiftUI
32
33
  import UIKit
33
- import DigiaEngage
34
34
 
35
35
  @objc(DigiaEngageModule)
36
36
  final class DigiaModule: RCTEventEmitter {
@@ -69,19 +69,22 @@ final class DigiaModule: RCTEventEmitter {
69
69
  resolve: @escaping RCTPromiseResolveBlock,
70
70
  reject: @escaping RCTPromiseRejectBlock
71
71
  ) {
72
- let envValue: DigiaEnvironment = environment.lowercased() == "sandbox" ? .sandbox : .production
72
+ let envValue: DigiaEnvironment =
73
+ environment.lowercased() == "sandbox" ? .sandbox : .production
73
74
  let logLevelValue: DigiaLogLevel
74
75
  switch logLevel.lowercased() {
75
76
  case "verbose": logLevelValue = .verbose
76
- case "none": logLevelValue = .none
77
- default: logLevelValue = .error
77
+ case "none": logLevelValue = .none
78
+ default: logLevelValue = .error
78
79
  }
79
80
 
80
81
  let config = DigiaConfig(
81
82
  apiKey: projectId,
82
83
  logLevel: logLevelValue,
83
84
  environment: envValue,
84
- developerConfig: baseUrl.flatMap { $0.isEmpty ? nil : DigiaDeveloperConfig(baseURL: $0) },
85
+ developerConfig: baseUrl.flatMap {
86
+ $0.isEmpty ? nil : DigiaDeveloperConfig(baseURL: $0)
87
+ },
85
88
  fontFamily: fontFamily.flatMap { $0.isEmpty ? nil : $0 }
86
89
  )
87
90
 
@@ -156,37 +159,28 @@ final class DigiaModule: RCTEventEmitter {
156
159
  }
157
160
 
158
161
  // ────────────────────────────────────────────────────────────────────────
159
- // MARK: - showAnchoredOverlay
162
+ // MARK: - registerAnchor / unregisterAnchor
160
163
 
161
- /// Shows SHOW_TOOLTIP or SHOW_SPOTLIGHT anchored to coordinates measured in JS.
162
- /// anchorX/Y/Width/Height are screen-pixel values from measureInWindow().
164
+ /// Registers a UI element as an anchor point for Guide experiences.
165
+ /// JS sends physical pixels; convert to UIKit points using screen scale.
163
166
  @objc
164
- func showAnchoredOverlay(
165
- _ id: String,
166
- content contentMap: NSDictionary,
167
- cepContext cepContextMap: NSDictionary,
168
- anchorX: Double,
169
- anchorY: Double,
170
- anchorWidth: Double,
171
- anchorHeight: Double
172
- ) {
173
- let mutable = NSMutableDictionary(dictionary: contentMap)
174
- // Pack anchor coords into the args sub-dict so buildInAppPayloadContent
175
- // captures them as JSONValue entries in InAppPayloadContent.args.
176
- var argsDict = (mutable["args"] as? [String: Any]) ?? [:]
177
- argsDict["_anchorX"] = anchorX
178
- argsDict["_anchorY"] = anchorY
179
- argsDict["_anchorWidth"] = anchorWidth
180
- argsDict["_anchorHeight"] = anchorHeight
181
- mutable["args"] = argsDict
182
-
183
- let content = buildInAppPayloadContent(from: mutable)
184
- let cepContext = (cepContextMap as? [String: String]) ?? [:]
185
- let payload = InAppPayload(id: id, content: content, cepContext: cepContext)
167
+ func registerAnchor(_ key: String, x: Double, y: Double, width: Double, height: Double) {
168
+ let scale = UIScreen.main.scale
169
+ let rect = CGRect(
170
+ x: CGFloat(x) / scale,
171
+ y: CGFloat(y) / scale,
172
+ width: CGFloat(width) / scale,
173
+ height: CGFloat(height) / scale
174
+ )
175
+ Task { @MainActor in
176
+ AnchorRegistry.shared.register(key: key, rect: rect)
177
+ }
178
+ }
186
179
 
180
+ @objc
181
+ func unregisterAnchor(_ key: String) {
187
182
  Task { @MainActor in
188
- guard let delegate = self.rnPlugin.delegate else { return }
189
- delegate.onCampaignTriggered(payload)
183
+ AnchorRegistry.shared.unregister(key: key)
190
184
  }
191
185
  }
192
186
 
@@ -199,44 +193,34 @@ final class DigiaModule: RCTEventEmitter {
199
193
  @MainActor
200
194
  private func mountDigiaHost() {
201
195
  // Locate the key window's root view controller.
202
- guard let rootVC = UIApplication.shared
203
- .connectedScenes
204
- .compactMap({ ($0 as? UIWindowScene)?.keyWindow })
205
- .first?
206
- .rootViewController else { return }
196
+ guard
197
+ let rootVC = UIApplication.shared
198
+ .connectedScenes
199
+ .compactMap({ ($0 as? UIWindowScene)?.keyWindow })
200
+ .first?
201
+ .rootViewController
202
+ else { return }
207
203
 
208
204
  // Guard against double-mounting (e.g. fast-refresh).
209
205
  let mountTag = 0xD19140
210
206
  if rootVC.view.viewWithTag(mountTag) != nil { return }
211
207
 
212
- // Container with passthrough hitTest — lets tooltip card taps reach SwiftUI
213
- // while all other touches fall through to React Native content.
214
- let container = DigiaPassthroughHostView()
215
- container.tag = mountTag
216
- container.translatesAutoresizingMaskIntoConstraints = false
217
- container.backgroundColor = .clear
218
-
219
208
  let hc = UIHostingController(rootView: DigiaHostWrapperView())
209
+ hc.view.tag = mountTag
220
210
  hc.view.translatesAutoresizingMaskIntoConstraints = false
221
211
  hc.view.backgroundColor = .clear
222
-
223
- container.hostingView = hc.view
212
+ // Pass touches through to React Native content below.
213
+ hc.view.isUserInteractionEnabled = false
224
214
 
225
215
  rootVC.addChild(hc)
226
- container.addSubview(hc.view)
216
+ rootVC.view.addSubview(hc.view)
227
217
  hc.didMove(toParent: rootVC)
228
218
 
229
- rootVC.view.addSubview(container)
230
-
231
219
  NSLayoutConstraint.activate([
232
- container.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
233
- container.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
234
- container.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
235
- container.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
236
- hc.view.leadingAnchor.constraint(equalTo: container.leadingAnchor),
237
- hc.view.trailingAnchor.constraint(equalTo: container.trailingAnchor),
238
- hc.view.topAnchor.constraint(equalTo: container.topAnchor),
239
- hc.view.bottomAnchor.constraint(equalTo: container.bottomAnchor),
220
+ hc.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
221
+ hc.view.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
222
+ hc.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
223
+ hc.view.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
240
224
  ])
241
225
  }
242
226
 
@@ -244,23 +228,23 @@ final class DigiaModule: RCTEventEmitter {
244
228
  // MARK: - Private helpers
245
229
 
246
230
  private func buildInAppPayloadContent(from map: NSDictionary) -> InAppPayloadContent {
247
- let pk = map["placementKey"] as? String
248
- let title = map["title"] as? String
249
- let text = map["text"] as? String
250
- let viewId = map["viewId"] as? String
251
- let command = map["command"] as? String
252
- let screenId = map["screenId"] as? String
231
+ let pk = map["placementKey"] as? String
232
+ let title = map["title"] as? String
233
+ let text = map["text"] as? String
234
+ let viewId = map["viewId"] as? String
235
+ let command = map["command"] as? String
236
+ let screenId = map["screenId"] as? String
253
237
  var type = (map["type"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
254
238
  if type.isEmpty {
255
- type = (pk?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "").isEmpty ? "dialog" : "inline"
239
+ type =
240
+ (pk?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "").isEmpty
241
+ ? "dialog" : "inline"
256
242
  }
257
243
  let args: [String: JSONValue] = {
258
244
  guard let raw = map["args"] as? [String: Any] else { return [:] }
259
245
  return raw.compactMapValues { JSONValue(rawValue: $0) }
260
246
  }()
261
247
 
262
- let anchorKey = map["anchorKey"] as? String
263
-
264
248
  return InAppPayloadContent(
265
249
  type: type,
266
250
  placementKey: pk,
@@ -269,36 +253,19 @@ final class DigiaModule: RCTEventEmitter {
269
253
  viewId: viewId,
270
254
  command: command,
271
255
  args: args,
272
- screenId: screenId,
273
- anchorKey: anchorKey
256
+ screenId: screenId
274
257
  )
275
258
  }
276
259
  }
277
260
 
278
- // MARK: - DigiaPassthroughHostView
279
-
280
- /// Container that delegates hitTest to the SwiftUI hosting view.
281
- /// When SwiftUI renders nothing interactive (no overlay), hitTest returns nil
282
- /// so touches fall through to React Native. When an overlay is visible,
283
- /// taps on it are consumed by SwiftUI.
284
- private final class DigiaPassthroughHostView: UIView {
285
- weak var hostingView: UIView?
286
-
287
- override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
288
- guard let hv = hostingView else { return nil }
289
- let converted = convert(point, to: hv)
290
- return hv.hitTest(converted, with: event)
291
- }
292
- }
293
-
294
261
  // MARK: - JSONValue convenience init from Any
295
- private extension JSONValue {
296
- init?(rawValue: Any) {
262
+ extension JSONValue {
263
+ fileprivate init?(rawValue: Any) {
297
264
  switch rawValue {
298
- case let s as String: self = .string(s)
299
- case let b as Bool: self = .bool(b)
300
- case let i as Int: self = .int(i)
301
- case let d as Double: self = .double(d)
265
+ case let s as String: self = .string(s)
266
+ case let b as Bool: self = .bool(b)
267
+ case let i as Int: self = .int(i)
268
+ case let d as Double: self = .double(d)
302
269
  case let arr as [Any]:
303
270
  self = .array(arr.compactMap { JSONValue(rawValue: $0) })
304
271
  case let dict as [String: Any]:
@@ -67,7 +67,8 @@ class DigiaClass {
67
67
  _actionHandler.digiaActionHandler.configure({
68
68
  onAction: config.onAction,
69
69
  routeViaSystemLinking: config.linking?.routeViaSystemLinking ?? true,
70
- inAppBrowser: config.linking?.inAppBrowser
70
+ inAppBrowser: config.linking?.inAppBrowser,
71
+ onFireEvent: (eventName, properties, context) => this._fireCustomEvent(eventName, properties, context)
71
72
  });
72
73
  try {
73
74
  await _NativeDigiaEngage.nativeDigiaModule.initialize(config.projectId, environment, logLevel, config.baseUrl, config.fontFamily);
@@ -245,6 +246,17 @@ class DigiaClass {
245
246
  if (this._engageSubscription) return;
246
247
  this._engageSubscription = _reactNative.DeviceEventEmitter.addListener('digiaEngageEvent', data => this._forwardExperienceEvent(data));
247
248
  }
249
+ _fireCustomEvent(eventName, properties, context) {
250
+ const payload = context ? this._activePayloads.get(context.campaign_id) : null;
251
+ if (payload) {
252
+ const event = {
253
+ type: 'clicked',
254
+ elementId: eventName
255
+ };
256
+ this._plugins.forEach(plugin => plugin.notifyEvent(event, payload));
257
+ }
258
+ // TODO: record custom event to Digia analytics endpoint when available
259
+ }
248
260
  _forwardExperienceEvent(data) {
249
261
  const payload = this._activePayloads.get(data.campaignId);
250
262
  if (!payload) return;
@@ -535,7 +547,7 @@ class DigiaClass {
535
547
  _log(message) {
536
548
  if (this._logLevel !== 'verbose') return;
537
549
  // eslint-disable-next-line no-console
538
- // console.log(`[Digia] ${message}`);
550
+ console.log(`[Digia] ${message}`);
539
551
  }
540
552
  }
541
553
  const Digia = exports.Digia = new DigiaClass();