@digia-engage/core 1.0.0-beta.4 → 1.0.0-beta.6

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 (44) hide show
  1. package/DigiaEngageReactNative.podspec +24 -8
  2. package/README.md +8 -17
  3. package/android/.project +28 -0
  4. package/android/build.gradle +1 -1
  5. package/android/settings.gradle +1 -3
  6. package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +1 -1
  7. package/android/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +146 -31
  8. package/ios/DigiaEngageModule.m +25 -44
  9. package/ios/DigiaHostViewManager.swift +128 -0
  10. package/ios/DigiaModule.swift +241 -0
  11. package/ios/DigiaSlotViewManager.swift +275 -0
  12. package/ios/RNEventBridgePlugin.swift +71 -0
  13. package/lib/commonjs/Digia.js +50 -0
  14. package/lib/commonjs/Digia.js.map +1 -1
  15. package/lib/commonjs/DigiaHostView.js +6 -50
  16. package/lib/commonjs/DigiaHostView.js.map +1 -1
  17. package/lib/commonjs/DigiaSlotView.js +37 -54
  18. package/lib/commonjs/DigiaSlotView.js.map +1 -1
  19. package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
  20. package/lib/module/Digia.js +50 -0
  21. package/lib/module/Digia.js.map +1 -1
  22. package/lib/module/DigiaHostView.js +6 -51
  23. package/lib/module/DigiaHostView.js.map +1 -1
  24. package/lib/module/DigiaSlotView.js +37 -52
  25. package/lib/module/DigiaSlotView.js.map +1 -1
  26. package/lib/module/NativeDigiaEngage.js.map +1 -1
  27. package/lib/typescript/Digia.d.ts +12 -0
  28. package/lib/typescript/Digia.d.ts.map +1 -1
  29. package/lib/typescript/DigiaHostView.d.ts +2 -28
  30. package/lib/typescript/DigiaHostView.d.ts.map +1 -1
  31. package/lib/typescript/DigiaSlotView.d.ts +3 -39
  32. package/lib/typescript/DigiaSlotView.d.ts.map +1 -1
  33. package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
  34. package/lib/typescript/index.d.ts +1 -1
  35. package/lib/typescript/index.d.ts.map +1 -1
  36. package/lib/typescript/types.d.ts +21 -0
  37. package/lib/typescript/types.d.ts.map +1 -1
  38. package/package.json +8 -18
  39. package/src/Digia.ts +60 -1
  40. package/src/DigiaHostView.tsx +7 -48
  41. package/src/DigiaSlotView.tsx +42 -49
  42. package/src/NativeDigiaEngage.ts +1 -0
  43. package/src/index.ts +1 -1
  44. package/src/types.ts +30 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * DigiaModule
3
+ *
4
+ * React Native NativeModule that bridges the Digia Engage iOS SDK.
5
+ *
6
+ * Exposed methods (callable from JS via NativeModules.DigiaEngageModule):
7
+ * initialize(apiKey, environment, logLevel): Promise<void>
8
+ * registerBridge(): void
9
+ * setCurrentScreen(name): void
10
+ * triggerCampaign(id, content, cepContext): void
11
+ * invalidateCampaign(campaignId): void
12
+ * createInitialPage(): void // AppConfig initial route
13
+ *
14
+ * Architecture
15
+ * ────────────
16
+ * The RN bridge mirrors the native Digia.initialize / Digia.register /
17
+ * Digia.setCurrentScreen flow exactly. An internal RNEventBridgePlugin is
18
+ * the single native DigiaCEPPlugin registered via Digia.register().
19
+ *
20
+ * When the SDK calls plugin.setup(delegate:), the bridge stores that
21
+ * delegate reference. JS plugins that deliver campaigns call triggerCampaign /
22
+ * invalidateCampaign which forward to delegate.onCampaignTriggered /
23
+ * delegate.onCampaignInvalidated.
24
+ *
25
+ * Overlay lifecycle events (impressed / clicked / dismissed) are forwarded from
26
+ * the native plugin.notifyEvent() to JS via RCTEventEmitter so that pure-JS
27
+ * CEP plugins (e.g. DigiaMoEngagePlugin) can report analytics.
28
+ */
29
+ import Foundation
30
+ import React
31
+ import SwiftUI
32
+ import UIKit
33
+ import DigiaEngage
34
+
35
+ @objc(DigiaEngageModule)
36
+ final class DigiaModule: RCTEventEmitter {
37
+
38
+ private lazy var rnPlugin: RNEventBridgePlugin = MainActor.assumeIsolated {
39
+ RNEventBridgePlugin(eventEmitter: self)
40
+ }
41
+
42
+ // ────────────────────────────────────────────────────────────────────────
43
+ // MARK: - RCTEventEmitter
44
+
45
+ override static func requiresMainQueueSetup() -> Bool { true }
46
+
47
+ override init() {
48
+ super.init()
49
+ // Pre-seed _listenerCount = 1 so sendEventWithName: never silently drops
50
+ // events when JS uses DeviceEventEmitter (which doesn't call native
51
+ // addListener: on iOS and therefore never increments the count).
52
+ addListener("digiaEngageEvent")
53
+ }
54
+
55
+ override func supportedEvents() -> [String]! {
56
+ return ["digiaEngageEvent"]
57
+ }
58
+
59
+ // ────────────────────────────────────────────────────────────────────────
60
+ // MARK: - initialize
61
+
62
+ @objc
63
+ func initialize(
64
+ _ apiKey: String,
65
+ environment: String,
66
+ logLevel: String,
67
+ resolve: @escaping RCTPromiseResolveBlock,
68
+ reject: @escaping RCTPromiseRejectBlock
69
+ ) {
70
+ let envValue: DigiaEnvironment = environment.lowercased() == "sandbox" ? .sandbox : .production
71
+ let logLevelValue: DigiaLogLevel
72
+ switch logLevel.lowercased() {
73
+ case "verbose": logLevelValue = .verbose
74
+ case "none": logLevelValue = .none
75
+ default: logLevelValue = .error
76
+ }
77
+
78
+ let config = DigiaConfig(
79
+ apiKey: apiKey,
80
+ logLevel: logLevelValue,
81
+ environment: envValue
82
+ )
83
+
84
+ Task { @MainActor in
85
+ do {
86
+ try await Digia.initialize(config)
87
+ self.mountDigiaHost()
88
+ resolve(nil)
89
+ } catch {
90
+ reject("DIGIA_INIT_ERROR", error.localizedDescription, error)
91
+ }
92
+ }
93
+ }
94
+
95
+ // ────────────────────────────────────────────────────────────────────────
96
+ // MARK: - registerBridge
97
+
98
+ /// Registers RNEventBridgePlugin with the native Digia SDK.
99
+ /// Called automatically by the JS Digia.register() wrapper on first plugin
100
+ /// registration so that the delegate is populated before any
101
+ /// triggerCampaign / invalidateCampaign calls arrive from JS.
102
+ @objc
103
+ func registerBridge() {
104
+ Task { @MainActor in
105
+ Digia.register(self.rnPlugin)
106
+ }
107
+ }
108
+
109
+ // ────────────────────────────────────────────────────────────────────────
110
+ // MARK: - setCurrentScreen
111
+
112
+ @objc
113
+ func setCurrentScreen(_ name: String) {
114
+ // Digia.setCurrentScreen is not yet part of the public iOS API; this is
115
+ // a no-op placeholder that can be activated once it is added.
116
+ // Digia.setCurrentScreen(name)
117
+ }
118
+
119
+ // ────────────────────────────────────────────────────────────────────────
120
+ // MARK: - triggerCampaign
121
+
122
+ /// Forwards a campaign payload to the native DigiaCEPDelegate.
123
+ ///
124
+ /// Called by the JS DigiaDelegate.onCampaignTriggered() when a JS CEP
125
+ /// plugin (e.g. DigiaMoEngagePlugin) delivers a campaign. The delegate
126
+ /// routes it into the SwiftUI overlay for rendering.
127
+ @objc
128
+ func triggerCampaign(
129
+ _ id: String,
130
+ content contentMap: NSDictionary,
131
+ cepContext cepContextMap: NSDictionary
132
+ ) {
133
+ let content = buildInAppPayloadContent(from: contentMap)
134
+ let cepContext = (cepContextMap as? [String: String]) ?? [:]
135
+ let payload = InAppPayload(id: id, content: content, cepContext: cepContext)
136
+
137
+ Task { @MainActor in
138
+ guard let delegate = self.rnPlugin.delegate else { return }
139
+ delegate.onCampaignTriggered(payload)
140
+ }
141
+ }
142
+
143
+ // ────────────────────────────────────────────────────────────────────────
144
+ // MARK: - invalidateCampaign
145
+
146
+ @objc
147
+ func invalidateCampaign(_ campaignId: String) {
148
+ Task { @MainActor in
149
+ guard let delegate = self.rnPlugin.delegate else { return }
150
+ delegate.onCampaignInvalidated(campaignId)
151
+ }
152
+ }
153
+
154
+ // ────────────────────────────────────────────────────────────────────────
155
+ // MARK: - Internal: mount the SwiftUI overlay host
156
+
157
+ /// Mirrors Android's DigiaModule.mountDigiaHost().
158
+ /// Called once after Digia.initialize() succeeds — no need for a manual
159
+ /// <DigiaHostView> anywhere in the JS component tree.
160
+ @MainActor
161
+ private func mountDigiaHost() {
162
+ // Locate the key window's root view controller.
163
+ guard let rootVC = UIApplication.shared
164
+ .connectedScenes
165
+ .compactMap({ ($0 as? UIWindowScene)?.keyWindow })
166
+ .first?
167
+ .rootViewController else { return }
168
+
169
+ // Guard against double-mounting (e.g. fast-refresh).
170
+ let mountTag = 0xD19140
171
+ if rootVC.view.viewWithTag(mountTag) != nil { return }
172
+
173
+ let hc = UIHostingController(rootView: DigiaHostWrapperView())
174
+ hc.view.tag = mountTag
175
+ hc.view.translatesAutoresizingMaskIntoConstraints = false
176
+ hc.view.backgroundColor = .clear
177
+ // Pass touches through to React Native content below.
178
+ hc.view.isUserInteractionEnabled = false
179
+
180
+ rootVC.addChild(hc)
181
+ rootVC.view.addSubview(hc.view)
182
+ hc.didMove(toParent: rootVC)
183
+
184
+ NSLayoutConstraint.activate([
185
+ hc.view.leadingAnchor.constraint(equalTo: rootVC.view.leadingAnchor),
186
+ hc.view.trailingAnchor.constraint(equalTo: rootVC.view.trailingAnchor),
187
+ hc.view.topAnchor.constraint(equalTo: rootVC.view.topAnchor),
188
+ hc.view.bottomAnchor.constraint(equalTo: rootVC.view.bottomAnchor),
189
+ ])
190
+ }
191
+
192
+ // ────────────────────────────────────────────────────────────────────────
193
+ // MARK: - Private helpers
194
+
195
+ private func buildInAppPayloadContent(from map: NSDictionary) -> InAppPayloadContent {
196
+ let pk = map["placementKey"] as? String
197
+ let title = map["title"] as? String
198
+ let text = map["text"] as? String
199
+ let viewId = map["viewId"] as? String
200
+ let command = map["command"] as? String
201
+ let screenId = map["screenId"] as? String
202
+ var type = (map["type"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
203
+ if type.isEmpty {
204
+ type = (pk?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "").isEmpty ? "dialog" : "inline"
205
+ }
206
+ let args: [String: JSONValue] = {
207
+ guard let raw = map["args"] as? [String: Any] else { return [:] }
208
+ return raw.compactMapValues { JSONValue(rawValue: $0) }
209
+ }()
210
+
211
+ return InAppPayloadContent(
212
+ type: type,
213
+ placementKey: pk,
214
+ title: title,
215
+ text: text,
216
+ viewId: viewId,
217
+ command: command,
218
+ args: args,
219
+ screenId: screenId
220
+ )
221
+ }
222
+ }
223
+
224
+ // MARK: - JSONValue convenience init from Any
225
+ private extension JSONValue {
226
+ init?(rawValue: Any) {
227
+ switch rawValue {
228
+ case let s as String: self = .string(s)
229
+ case let b as Bool: self = .bool(b)
230
+ case let i as Int: self = .int(i)
231
+ case let d as Double: self = .double(d)
232
+ case let arr as [Any]:
233
+ self = .array(arr.compactMap { JSONValue(rawValue: $0) })
234
+ case let dict as [String: Any]:
235
+ let mapped = dict.compactMapValues { JSONValue(rawValue: $0) }
236
+ self = .object(mapped)
237
+ default:
238
+ return nil
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,275 @@
1
+ import SwiftUI
2
+ import React
3
+ import DigiaEngage
4
+
5
+ @objc(DigiaSlotView)
6
+ final class DigiaSlotViewManager: RCTViewManager {
7
+
8
+ override static func requiresMainQueueSetup() -> Bool { true }
9
+
10
+ override func view() -> UIView! {
11
+ return DigiaSlotUIView()
12
+ }
13
+
14
+ @objc func setPlacementKey(_ placementKey: String, forView view: DigiaSlotUIView) {
15
+ view.placementKey = placementKey
16
+ }
17
+ }
18
+
19
+ // MARK: - DigiaSlotUIView
20
+
21
+ /// UIView container that embeds DigiaSlot<EmptyView> in a UIHostingController.
22
+ /// Re-creates the SwiftUI view when placementKey changes.
23
+ final class DigiaSlotUIView: UIView {
24
+
25
+ override init(frame: CGRect) {
26
+ super.init(frame: frame)
27
+ clipsToBounds = false
28
+ }
29
+
30
+ required init?(coder: NSCoder) {
31
+ super.init(coder: coder)
32
+ clipsToBounds = false
33
+ }
34
+
35
+ @objc var placementKey: String = "" {
36
+ didSet {
37
+ guard placementKey != oldValue else { return }
38
+ lastReportedHeight = -.infinity
39
+ remount()
40
+ }
41
+ }
42
+
43
+ /// Fired when SwiftUI slot content intrinsic height changes (parity with Android `onContentSizeChange`).
44
+ @objc var onContentSizeChange: RCTDirectEventBlock?
45
+
46
+ private var hostingController: SlotHostingController<DigiaSlotWrapperView>?
47
+ private var lastReportedHeight: CGFloat = -.infinity
48
+ private var delegateProxiesInstalled = false
49
+
50
+ override func didMoveToWindow() {
51
+ super.didMoveToWindow()
52
+ if window != nil {
53
+ if hostingController == nil { remount() }
54
+ // Deferred so the full RN view hierarchy (including the Fabric
55
+ // surface root with RCTSurfaceTouchHandler) is established.
56
+ DispatchQueue.main.async { [weak self] in
57
+ self?.installDelegateProxiesIfNeeded()
58
+ }
59
+ } else {
60
+ teardown()
61
+ }
62
+ }
63
+
64
+ override func layoutSubviews() {
65
+ super.layoutSubviews()
66
+ emitContentSizeIfNeeded()
67
+ }
68
+
69
+ // -- Touch handling -------------------------------------------------------
70
+ // RCTSurfaceTouchHandler claims touches before SwiftUI can handle them.
71
+ // RCTTouchDelegateProxy wraps its delegate and blocks touches that land
72
+ // within a registered hosting view, so SwiftUI gestures fire normally.
73
+
74
+ /// Expand the tappable area to include the hosting view when it overflows self.bounds.
75
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
76
+ if super.point(inside: point, with: event) { return true }
77
+ guard let hcView = hostingController?.view else { return false }
78
+ let converted = convert(point, to: hcView)
79
+ return hcView.point(inside: converted, with: event)
80
+ }
81
+
82
+ /// Forward hit-testing to SwiftUI view hierarchy even outside self.bounds.
83
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
84
+ guard let hcView = hostingController?.view, isUserInteractionEnabled, !isHidden, alpha > 0.01 else {
85
+ return super.hitTest(point, with: event)
86
+ }
87
+ let converted = convert(point, to: hcView)
88
+ if let target = hcView.hitTest(converted, with: event) {
89
+ return target
90
+ }
91
+ return super.hitTest(point, with: event)
92
+ }
93
+
94
+ /// Walk ancestors to find RCT touch handlers and install the delegate proxy.
95
+ private func installDelegateProxiesIfNeeded() {
96
+ guard !delegateProxiesInstalled, let hcView = hostingController?.view else { return }
97
+ delegateProxiesInstalled = true
98
+
99
+ RCTTouchDelegateProxy.registerHostingView(hcView)
100
+
101
+ var ancestor: UIView? = superview
102
+ while let v = ancestor {
103
+ for gr in v.gestureRecognizers ?? [] {
104
+ let name = String(describing: type(of: gr))
105
+ if name.hasPrefix("RCT"), name.lowercased().contains("touch") {
106
+ RCTTouchDelegateProxy.installIfNeeded(on: gr)
107
+ }
108
+ }
109
+ ancestor = v.superview
110
+ }
111
+ }
112
+
113
+ private func remount() {
114
+ teardown()
115
+ guard window != nil, !placementKey.isEmpty else { return }
116
+ guard let parentVC = parentViewController() else { return }
117
+
118
+ let swiftUIView = DigiaSlotWrapperView(placementKey: placementKey)
119
+ let hc = SlotHostingController(rootView: swiftUIView)
120
+ hc.sizingOptions = .intrinsicContentSize
121
+ hc.view.translatesAutoresizingMaskIntoConstraints = false
122
+ hc.view.backgroundColor = .clear
123
+ hc.onLayoutChange = { [weak self] in
124
+ self?.emitContentSizeIfNeeded()
125
+ }
126
+
127
+ parentVC.addChild(hc)
128
+ addSubview(hc.view)
129
+ hc.didMove(toParent: parentVC)
130
+
131
+ // No bottom constraint — lets SwiftUI report intrinsic height via onContentSizeChange.
132
+ NSLayoutConstraint.activate([
133
+ hc.view.leadingAnchor.constraint(equalTo: leadingAnchor),
134
+ hc.view.trailingAnchor.constraint(equalTo: trailingAnchor),
135
+ hc.view.topAnchor.constraint(equalTo: topAnchor),
136
+ ])
137
+
138
+ hostingController = hc
139
+ DispatchQueue.main.async { [weak self] in
140
+ self?.emitContentSizeIfNeeded()
141
+ }
142
+ }
143
+
144
+ private func teardown() {
145
+ if let hcView = hostingController?.view {
146
+ RCTTouchDelegateProxy.unregisterHostingView(hcView)
147
+ }
148
+ delegateProxiesInstalled = false
149
+
150
+ hostingController?.willMove(toParent: nil)
151
+ hostingController?.view.removeFromSuperview()
152
+ hostingController?.removeFromParent()
153
+ hostingController = nil
154
+ lastReportedHeight = -.infinity
155
+ }
156
+
157
+ /// Emit content height to JS so it can resize the RN wrapper.
158
+ private func emitContentSizeIfNeeded() {
159
+ guard let hc = hostingController, let block = onContentSizeChange else { return }
160
+ let width = bounds.width
161
+ guard width > 0 else { return }
162
+
163
+ hc.view.layoutIfNeeded()
164
+ let intrinsic = hc.view.intrinsicContentSize.height
165
+ let height: CGFloat
166
+ if intrinsic.isFinite, intrinsic > 0, intrinsic != UIView.noIntrinsicMetric {
167
+ height = intrinsic
168
+ } else {
169
+ height = hc.view.systemLayoutSizeFitting(
170
+ CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
171
+ withHorizontalFittingPriority: .required,
172
+ verticalFittingPriority: .fittingSizeLevel
173
+ ).height
174
+ }
175
+
176
+ if abs(height - lastReportedHeight) < 0.5 { return }
177
+ lastReportedHeight = height
178
+ block(["height": height as NSNumber])
179
+ }
180
+
181
+ /// Walk the view hierarchy to find the nearest UIViewController.
182
+ private func parentViewController() -> UIViewController? {
183
+ let reactSel = NSSelectorFromString("reactViewController")
184
+ var view: UIView? = self
185
+ while let v = view {
186
+ if v.responds(to: reactSel), let raw = v.perform(reactSel)?.takeUnretainedValue() {
187
+ if let vc = raw as? UIViewController { return vc }
188
+ }
189
+ view = v.superview
190
+ }
191
+ view = self
192
+ while let v = view {
193
+ var r: UIResponder? = v.next
194
+ while let responder = r {
195
+ if let vc = responder as? UIViewController { return vc }
196
+ r = responder.next
197
+ }
198
+ view = v.superview
199
+ }
200
+ return nil
201
+ }
202
+ }
203
+
204
+ // MARK: - SwiftUI wrapper
205
+
206
+ private struct DigiaSlotWrapperView: View {
207
+ let placementKey: String
208
+
209
+ var body: some View {
210
+ DigiaSlot(placementKey)
211
+ }
212
+ }
213
+
214
+ // MARK: - Hosting controller subclass
215
+
216
+ /// Overrides `viewDidLayoutSubviews` so that SwiftUI content size changes
217
+ /// (e.g. campaign arriving into `InlineCampaignController`) propagate back
218
+ /// to the UIKit `DigiaSlotUIView` which reports the new height to RN.
219
+ private final class SlotHostingController<Content: View>: UIHostingController<Content> {
220
+ var onLayoutChange: (() -> Void)?
221
+
222
+ override func viewDidLayoutSubviews() {
223
+ super.viewDidLayoutSubviews()
224
+ onLayoutChange?()
225
+ }
226
+ }
227
+
228
+ // MARK: - RCT touch delegate proxy
229
+
230
+ private final class RCTTouchDelegateProxy: NSObject, UIGestureRecognizerDelegate {
231
+
232
+ private static let hostingViews = NSHashTable<UIView>.weakObjects()
233
+ private static let proxies = NSMapTable<UIGestureRecognizer, RCTTouchDelegateProxy>.weakToStrongObjects()
234
+
235
+ static func registerHostingView(_ view: UIView) {
236
+ hostingViews.add(view)
237
+ }
238
+
239
+ static func unregisterHostingView(_ view: UIView) {
240
+ hostingViews.remove(view)
241
+ }
242
+
243
+ static func installIfNeeded(on gr: UIGestureRecognizer) {
244
+ guard proxies.object(forKey: gr) == nil else { return }
245
+ let proxy = RCTTouchDelegateProxy()
246
+ proxy.originalDelegate = gr.delegate
247
+ gr.delegate = proxy
248
+ proxies.setObject(proxy, forKey: gr)
249
+ }
250
+
251
+ weak var originalDelegate: UIGestureRecognizerDelegate?
252
+
253
+ func gestureRecognizer(_ gr: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
254
+ if let touchView = touch.view {
255
+ for hv in Self.hostingViews.allObjects {
256
+ if touchView === hv || touchView.isDescendant(of: hv) {
257
+ return false
258
+ }
259
+ }
260
+ }
261
+ return originalDelegate?.gestureRecognizer?(gr, shouldReceive: touch) ?? true
262
+ }
263
+
264
+ override func responds(to aSelector: Selector!) -> Bool {
265
+ if super.responds(to: aSelector) { return true }
266
+ return originalDelegate?.responds(to: aSelector) ?? false
267
+ }
268
+
269
+ override func forwardingTarget(for aSelector: Selector!) -> Any? {
270
+ if let original = originalDelegate, original.responds(to: aSelector) {
271
+ return original
272
+ }
273
+ return super.forwardingTarget(for: aSelector)
274
+ }
275
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * RNEventBridgePlugin
3
+ *
4
+ * Implements DigiaCEPPlugin so it can be registered with the native iOS Digia
5
+ * SDK via Digia.register(). It acts as the bridge between the native SDK's
6
+ * DigiaCEPDelegate and the JS layer.
7
+ *
8
+ * Lifecycle
9
+ * ─────────
10
+ * 1. JS calls Digia.register() → native registerBridge() → this plugin is
11
+ * registered with the native SDK via Digia.register(rnPlugin).
12
+ * 2. The SDK calls setup(delegate:) storing the delegate reference.
13
+ * 3. When a JS CEP plugin delivers a campaign via onCampaignTriggered(),
14
+ * the JS side calls triggerCampaign() which forwards here via DigiaModule.
15
+ * 4. Overlay lifecycle events (impressed / clicked / dismissed) travel back to
16
+ * JS as DeviceEventEmitter events.
17
+ */
18
+ import Foundation
19
+ import React
20
+ import DigiaEngage
21
+
22
+ @objc
23
+ internal final class RNEventBridgePlugin: NSObject, DigiaCEPPlugin {
24
+
25
+ let identifier = "com.digia.rn.bridge"
26
+
27
+ /// Populated by the SDK when registered. Used to forward JS-side campaigns
28
+ /// into the native overlay rendering pipeline.
29
+ private(set) weak var delegate: DigiaCEPDelegate?
30
+
31
+ private weak var eventEmitter: RCTEventEmitter?
32
+
33
+ init(eventEmitter: RCTEventEmitter?) {
34
+ self.eventEmitter = eventEmitter
35
+ }
36
+
37
+ // MARK: - DigiaCEPPlugin
38
+
39
+ func setup(delegate: DigiaCEPDelegate) {
40
+ self.delegate = delegate
41
+ }
42
+
43
+ func notifyEvent(_ event: DigiaExperienceEvent, payload: InAppPayload) {
44
+
45
+ var body: [String: Any] = ["campaignId": payload.id]
46
+ switch event {
47
+ case .impressed:
48
+ body["type"] = "impressed"
49
+ case .clicked(let elementID):
50
+ body["type"] = "clicked"
51
+ if let elementID {
52
+ body["elementId"] = elementID
53
+ }
54
+ case .dismissed:
55
+ body["type"] = "dismissed"
56
+ }
57
+ eventEmitter?.sendEvent(withName: "digiaEngageEvent", body: body)
58
+ }
59
+
60
+ func healthCheck() -> DiagnosticReport {
61
+ return DiagnosticReport(
62
+ isHealthy: delegate != nil,
63
+ issue: delegate == nil ? "No delegate set" : nil,
64
+ resolution: delegate == nil ? "Call registerBridge() before triggerCampaign()" : nil
65
+ )
66
+ }
67
+
68
+ func teardown() {
69
+ delegate = nil
70
+ }
71
+ }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.Digia = void 0;
7
+ var _reactNative = require("react-native");
7
8
  var _NativeDigiaEngage = require("./NativeDigiaEngage");
8
9
  /**
9
10
  * High-level Digia Engage SDK wrapper.
@@ -27,6 +28,10 @@ class DigiaClass {
27
28
  // Tracks whether the native bridge plugin (RNEventBridgePlugin) has been
28
29
  // wired to the native SDK. Done once on the first Digia.register() call.
29
30
  _nativeBridgeWired = false;
31
+ // Cache of triggered payloads keyed by campaign ID, used to reconstruct
32
+ // the full InAppPayload when overlay lifecycle events arrive from native.
33
+ _activePayloads = new Map();
34
+ _engageSubscription = null;
30
35
 
31
36
  /**
32
37
  * Initialise the Digia Engage SDK.
@@ -63,6 +68,7 @@ class DigiaClass {
63
68
  // so the delegate is ready when JS campaigns start flowing.
64
69
  if (!this._nativeBridgeWired) {
65
70
  _NativeDigiaEngage.nativeDigiaModule.registerBridge();
71
+ this._startEngageListener();
66
72
  this._nativeBridgeWired = true;
67
73
  }
68
74
  plugin.setup(this);
@@ -96,11 +102,55 @@ class DigiaClass {
96
102
  // Forwards to the native DigiaCEPDelegate via the bridge.
97
103
 
98
104
  onCampaignTriggered(payload) {
105
+ this._activePayloads.set(payload.id, payload);
99
106
  _NativeDigiaEngage.nativeDigiaModule.triggerCampaign(payload.id, payload.content, payload.cepContext);
100
107
  }
101
108
  onCampaignInvalidated(campaignId) {
109
+ this._activePayloads.delete(campaignId);
102
110
  _NativeDigiaEngage.nativeDigiaModule.invalidateCampaign(campaignId);
103
111
  }
112
+
113
+ // ── Overlay event forwarding ─────────────────────────────────────────────
114
+
115
+ /**
116
+ * Subscribes to `digiaOverlayEvent` emitted by the native
117
+ * RNEventBridgePlugin when the Compose overlay fires a lifecycle event
118
+ * (impressed / clicked / dismissed).
119
+ *
120
+ * Each event is forwarded to every registered plugin's notifyEvent() so
121
+ * that CEP plugins (e.g. WebEngagePlugin) can report analytics.
122
+ */
123
+ _startEngageListener() {
124
+ if (this._engageSubscription) return;
125
+ this._engageSubscription = _reactNative.DeviceEventEmitter.addListener('digiaEngageEvent', data => this._forwardExperienceEvent(data));
126
+ }
127
+ _forwardExperienceEvent(data) {
128
+ const payload = this._activePayloads.get(data.campaignId);
129
+ if (!payload) return;
130
+ let event;
131
+ switch (data.type) {
132
+ case 'impressed':
133
+ event = {
134
+ type: 'impressed'
135
+ };
136
+ break;
137
+ case 'clicked':
138
+ event = {
139
+ type: 'clicked',
140
+ elementId: data.elementId
141
+ };
142
+ break;
143
+ case 'dismissed':
144
+ event = {
145
+ type: 'dismissed'
146
+ };
147
+ this._activePayloads.delete(data.campaignId);
148
+ break;
149
+ default:
150
+ return;
151
+ }
152
+ this._plugins.forEach(plugin => plugin.notifyEvent(event, payload));
153
+ }
104
154
  }
105
155
  const Digia = exports.Digia = new DigiaClass();
106
156
  //# sourceMappingURL=Digia.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_NativeDigiaEngage","require","DigiaClass","_plugins","Map","_nativeBridgeWired","initialize","config","environment","logLevel","nativeDigiaModule","apiKey","register","plugin","has","identifier","get","teardown","registerBridge","setup","set","unregister","pluginOrId","id","delete","setCurrentScreen","name","forEach","forwardScreen","onCampaignTriggered","payload","triggerCampaign","content","cepContext","onCampaignInvalidated","campaignId","invalidateCampaign","Digia","exports"],"sourceRoot":"../../src","sources":["Digia.ts"],"mappings":";;;;;;AAiBA,IAAAA,kBAAA,GAAAC,OAAA;AAjBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAKA,MAAMC,UAAU,CAA0B;EACrBC,QAAQ,GAAG,IAAIC,GAAG,CAAsB,CAAC;EAC1D;EACA;EACQC,kBAAkB,GAAG,KAAK;;EAElC;AACJ;AACA;AACA;AACA;AACA;EACI,MAAMC,UAAUA,CAACC,MAAmB,EAAiB;IACjD,MAAMC,WAAW,GAAGD,MAAM,CAACC,WAAW,IAAI,YAAY;IACtD,MAAMC,QAAQ,GAAGF,MAAM,CAACE,QAAQ,IAAI,OAAO;IAC3C,MAAMC,oCAAiB,CAACJ,UAAU,CAACC,MAAM,CAACI,MAAM,EAAEH,WAAW,EAAEC,QAAQ,CAAC;EAC5E;;EAEA;AACJ;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;EACIG,QAAQA,CAACC,MAAmB,EAAQ;IAChC,IAAI,IAAI,CAACV,QAAQ,CAACW,GAAG,CAACD,MAAM,CAACE,UAAU,CAAC,EAAE;MACtC,IAAI,CAACZ,QAAQ,CAACa,GAAG,CAACH,MAAM,CAACE,UAAU,CAAC,CAAEE,QAAQ,CAAC,CAAC;IACpD;IACA;IACA;IACA,IAAI,CAAC,IAAI,CAACZ,kBAAkB,EAAE;MAC1BK,oCAAiB,CAACQ,cAAc,CAAC,CAAC;MAClC,IAAI,CAACb,kBAAkB,GAAG,IAAI;IAClC;IACAQ,MAAM,CAACM,KAAK,CAAC,IAAI,CAAC;IAClB,IAAI,CAAChB,QAAQ,CAACiB,GAAG,CAACP,MAAM,CAACE,UAAU,EAAEF,MAAM,CAAC;EAChD;;EAEA;AACJ;AACA;AACA;EACIQ,UAAUA,CAACC,UAAgC,EAAQ;IAC/C,MAAMC,EAAE,GAAG,OAAOD,UAAU,KAAK,QAAQ,GAAGA,UAAU,GAAGA,UAAU,CAACP,UAAU;IAC9E,IAAI,CAACZ,QAAQ,CAACa,GAAG,CAACO,EAAE,CAAC,EAAEN,QAAQ,CAAC,CAAC;IACjC,IAAI,CAACd,QAAQ,CAACqB,MAAM,CAACD,EAAE,CAAC;EAC5B;;EAEA;AACJ;AACA;AACA;AACA;AACA;AACA;EACIE,gBAAgBA,CAACC,IAAY,EAAQ;IACjChB,oCAAiB,CAACe,gBAAgB,CAACC,IAAI,CAAC;IACxC,IAAI,CAACvB,QAAQ,CAACwB,OAAO,CAAEd,MAAM,IAAKA,MAAM,CAACe,aAAa,CAACF,IAAI,CAAC,CAAC;EACjE;;EAEA;EACA;EACA;;EAEAG,mBAAmBA,CAACC,OAAqB,EAAQ;IAC7CpB,oCAAiB,CAACqB,eAAe,CAACD,OAAO,CAACP,EAAE,EAAEO,OAAO,CAACE,OAAO,EAAEF,OAAO,CAACG,UAAU,CAAC;EACtF;EAEAC,qBAAqBA,CAACC,UAAkB,EAAQ;IAC5CzB,oCAAiB,CAAC0B,kBAAkB,CAACD,UAAU,CAAC;EACpD;AAEJ;AAEO,MAAME,KAAK,GAAAC,OAAA,CAAAD,KAAA,GAAG,IAAInC,UAAU,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_reactNative","require","_NativeDigiaEngage","DigiaClass","_plugins","Map","_nativeBridgeWired","_activePayloads","_engageSubscription","initialize","config","environment","logLevel","nativeDigiaModule","apiKey","register","plugin","has","identifier","get","teardown","registerBridge","_startEngageListener","setup","set","unregister","pluginOrId","id","delete","setCurrentScreen","name","forEach","forwardScreen","onCampaignTriggered","payload","triggerCampaign","content","cepContext","onCampaignInvalidated","campaignId","invalidateCampaign","DeviceEventEmitter","addListener","data","_forwardExperienceEvent","event","type","elementId","notifyEvent","Digia","exports"],"sourceRoot":"../../src","sources":["Digia.ts"],"mappings":"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","ignoreList":[]}