@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.
- package/DigiaEngageReactNative.podspec +24 -8
- package/README.md +8 -17
- package/android/.project +28 -0
- package/android/build.gradle +1 -1
- package/android/settings.gradle +1 -3
- package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +1 -1
- package/android/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +146 -31
- package/ios/DigiaEngageModule.m +25 -44
- package/ios/DigiaHostViewManager.swift +128 -0
- package/ios/DigiaModule.swift +241 -0
- package/ios/DigiaSlotViewManager.swift +275 -0
- package/ios/RNEventBridgePlugin.swift +71 -0
- package/lib/commonjs/Digia.js +50 -0
- package/lib/commonjs/Digia.js.map +1 -1
- package/lib/commonjs/DigiaHostView.js +6 -50
- package/lib/commonjs/DigiaHostView.js.map +1 -1
- package/lib/commonjs/DigiaSlotView.js +37 -54
- package/lib/commonjs/DigiaSlotView.js.map +1 -1
- package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
- package/lib/module/Digia.js +50 -0
- package/lib/module/Digia.js.map +1 -1
- package/lib/module/DigiaHostView.js +6 -51
- package/lib/module/DigiaHostView.js.map +1 -1
- package/lib/module/DigiaSlotView.js +37 -52
- package/lib/module/DigiaSlotView.js.map +1 -1
- package/lib/module/NativeDigiaEngage.js.map +1 -1
- package/lib/typescript/Digia.d.ts +12 -0
- package/lib/typescript/Digia.d.ts.map +1 -1
- package/lib/typescript/DigiaHostView.d.ts +2 -28
- package/lib/typescript/DigiaHostView.d.ts.map +1 -1
- package/lib/typescript/DigiaSlotView.d.ts +3 -39
- package/lib/typescript/DigiaSlotView.d.ts.map +1 -1
- package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +21 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +8 -18
- package/src/Digia.ts +60 -1
- package/src/DigiaHostView.tsx +7 -48
- package/src/DigiaSlotView.tsx +42 -49
- package/src/NativeDigiaEngage.ts +1 -0
- package/src/index.ts +1 -1
- 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
|
+
}
|
package/lib/commonjs/Digia.js
CHANGED
|
@@ -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":["
|
|
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":[]}
|