@digia-engage/core 2.1.0 → 2.2.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.
- package/DigiaEngageReactNative.podspec +1 -1
- package/android/.project +0 -6
- package/android/bin/.gradle/8.13/fileHashes/fileHashes.lock +0 -0
- package/android/bin/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/bin/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/bin/.project +34 -0
- package/android/bin/build/generated/source/buildConfig/debug/com/digia/engage/rn/BuildConfig.class +0 -0
- package/android/bin/build/generated/source/codegen/java/com/digia/engage/rn/NativeDigiaEngageSpec.class +0 -0
- package/android/bin/build.gradle +97 -0
- package/android/bin/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/bin/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/android/bin/gradle.properties +2 -0
- package/android/bin/gradlew +185 -0
- package/android/bin/gradlew.bat +89 -0
- package/android/bin/local.properties +1 -0
- package/android/bin/settings.gradle +25 -0
- package/android/bin/src/main/AndroidManifest.xml +2 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaAnchorViewManager.kt +90 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaModule.kt +309 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaPackage.kt +70 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +183 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaViewManager.kt +64 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +37 -13
- package/ios/DigiaEngageModule.m +25 -26
- package/ios/DigiaModule.swift +70 -11
- package/lib/commonjs/Digia.js +13 -2
- package/lib/commonjs/Digia.js.map +1 -1
- package/lib/commonjs/DigiaGuideController.js.map +1 -1
- package/lib/commonjs/DigiaProvider.js +38 -22
- package/lib/commonjs/DigiaProvider.js.map +1 -1
- package/lib/commonjs/interpolate.js +41 -0
- package/lib/commonjs/interpolate.js.map +1 -0
- package/lib/module/Digia.js +13 -2
- package/lib/module/Digia.js.map +1 -1
- package/lib/module/DigiaGuideController.js.map +1 -1
- package/lib/module/DigiaProvider.js +40 -23
- package/lib/module/DigiaProvider.js.map +1 -1
- package/lib/module/interpolate.js +34 -0
- package/lib/module/interpolate.js.map +1 -0
- package/lib/typescript/Digia.d.ts +1 -0
- package/lib/typescript/Digia.d.ts.map +1 -1
- package/lib/typescript/DigiaGuideController.d.ts +2 -0
- package/lib/typescript/DigiaGuideController.d.ts.map +1 -1
- package/lib/typescript/DigiaProvider.d.ts.map +1 -1
- package/lib/typescript/interpolate.d.ts +4 -0
- package/lib/typescript/interpolate.d.ts.map +1 -0
- package/package.json +1 -1
- package/react-native.config.js +1 -1
- package/src/Digia.ts +15 -2
- package/src/DigiaAnchorView.tsx +1 -0
- package/src/DigiaGuideController.ts +2 -0
- package/src/DigiaProvider.tsx +160 -140
- package/src/interpolate.ts +44 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigiaModule
|
|
3
|
+
*
|
|
4
|
+
* React Native NativeModule that bridges the Digia Engage Android SDK.
|
|
5
|
+
*
|
|
6
|
+
* Exposed methods (callable from JS via NativeModules.DigiaEngageModule):
|
|
7
|
+
*
|
|
8
|
+
* initialize(apiKey, environment, logLevel, baseUrl): Promise<void> register(): void
|
|
9
|
+
* setCurrentScreen(name): void triggerCampaign(id, content, cepContext): void
|
|
10
|
+
* triggerCampaignById(campaignId): void invalidateCampaign(campaignId): void
|
|
11
|
+
*
|
|
12
|
+
* Architecture ──────────── The RN bridge mirrors the native Digia.initialize / Digia.register /
|
|
13
|
+
* Digia.setCurrentScreen flow exactly. An internal [RNEventBridgePlugin] is the single native
|
|
14
|
+
* DigiaCEPPlugin registered via Digia.register().
|
|
15
|
+
*
|
|
16
|
+
* When the SDK calls plugin.setup(delegate), the bridge stores that delegate reference. JS plugins
|
|
17
|
+
* that need to push campaigns into the Compose overlay call triggerCampaign / invalidateCampaign
|
|
18
|
+
* which forward to delegate.onCampaignTriggered / delegate.onCampaignInvalidated.
|
|
19
|
+
*
|
|
20
|
+
* Overlay lifecycle events (impressed / clicked / dismissed) are forwarded from the native
|
|
21
|
+
* plugin.notifyEvent() to JS via DeviceEventEmitter so that pure-JS CEP plugins (e.g.
|
|
22
|
+
* DigiaMoEngagePlugin) can report analytics.
|
|
23
|
+
*/
|
|
24
|
+
package com.digia.engage.rn
|
|
25
|
+
|
|
26
|
+
import androidx.lifecycle.LifecycleOwner
|
|
27
|
+
import androidx.lifecycle.ViewModelStoreOwner
|
|
28
|
+
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
29
|
+
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
|
30
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
31
|
+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
32
|
+
import com.digia.engage.DiagnosticReport
|
|
33
|
+
import com.digia.engage.Digia
|
|
34
|
+
import com.digia.engage.DigiaCEPDelegate
|
|
35
|
+
import com.digia.engage.DigiaCEPPlugin
|
|
36
|
+
import com.digia.engage.DigiaConfig
|
|
37
|
+
import com.digia.engage.DigiaEnvironment
|
|
38
|
+
import com.digia.engage.DigiaExperienceEvent
|
|
39
|
+
import com.digia.engage.DigiaHostView
|
|
40
|
+
import com.digia.engage.DigiaLogLevel
|
|
41
|
+
import com.digia.engage.InAppPayload
|
|
42
|
+
import com.facebook.react.bridge.Arguments
|
|
43
|
+
import com.facebook.react.bridge.Promise
|
|
44
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
45
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
46
|
+
import com.facebook.react.bridge.ReactMethod
|
|
47
|
+
import com.facebook.react.bridge.ReadableMap
|
|
48
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
49
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
50
|
+
|
|
51
|
+
internal class DigiaModule(
|
|
52
|
+
private val reactContext: ReactApplicationContext,
|
|
53
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
54
|
+
|
|
55
|
+
private val rnPlugin = RNEventBridgePlugin(reactContext)
|
|
56
|
+
|
|
57
|
+
override fun getName(): String = MODULE_NAME
|
|
58
|
+
|
|
59
|
+
// ─── initialize ───────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
@ReactMethod
|
|
62
|
+
fun initialize(
|
|
63
|
+
apiKey: String,
|
|
64
|
+
environment: String,
|
|
65
|
+
logLevel: String,
|
|
66
|
+
baseUrl: String?,
|
|
67
|
+
fontFamily: String?,
|
|
68
|
+
promise: Promise
|
|
69
|
+
) {
|
|
70
|
+
try {
|
|
71
|
+
val cleanBaseUrl =
|
|
72
|
+
baseUrl.trim().trimEnd('/').removeSuffix("/api/v1").takeIf { it.isNotBlank() }
|
|
73
|
+
val config =
|
|
74
|
+
DigiaConfig(
|
|
75
|
+
apiKey = apiKey,
|
|
76
|
+
baseUrl = cleanBaseUrl,
|
|
77
|
+
environment =
|
|
78
|
+
when (environment.lowercase()) {
|
|
79
|
+
"sandbox" -> DigiaEnvironment.SANDBOX
|
|
80
|
+
else -> DigiaEnvironment.PRODUCTION
|
|
81
|
+
},
|
|
82
|
+
logLevel =
|
|
83
|
+
when (logLevel.lowercase()) {
|
|
84
|
+
"verbose" -> DigiaLogLevel.VERBOSE
|
|
85
|
+
"none" -> DigiaLogLevel.NONE
|
|
86
|
+
else -> DigiaLogLevel.ERROR
|
|
87
|
+
},
|
|
88
|
+
baseUrl = baseUrl?.takeIf { it.isNotBlank() },
|
|
89
|
+
fontFamily = fontFamily?.takeIf { it.isNotBlank() },
|
|
90
|
+
)
|
|
91
|
+
Digia.initialize(reactContext.applicationContext, config)
|
|
92
|
+
|
|
93
|
+
UiThreadUtil.runOnUiThread {
|
|
94
|
+
// Mount the Compose overlay ABOVE the ReactRootView via addContentView().
|
|
95
|
+
// This keeps it outside Fabric's shadow tree entirely so Fabric hit-testing
|
|
96
|
+
// never sees it. Touch pass-through is handled by DigiaHostView.dispatchTouchEvent
|
|
97
|
+
// returning false at the native Android level — which works correctly at this
|
|
98
|
+
// level of the hierarchy, unlike inside Fabric where pointerEvents="none" on
|
|
99
|
+
// non-ReactViewGroup views is not respected during shadow-tree hit-testing.
|
|
100
|
+
mountDigiaHost()
|
|
101
|
+
promise.resolve(null)
|
|
102
|
+
}
|
|
103
|
+
} catch (e: Exception) {
|
|
104
|
+
promise.reject("DIGIA_INIT_ERROR", e.message ?: "Initialisation failed", e)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── register ─────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Registers [RNEventBridgePlugin] with the native Digia SDK.
|
|
112
|
+
*
|
|
113
|
+
* This is bridge infrastructure — not a user-facing CEP plugin. Called once by the JS
|
|
114
|
+
* `Digia.register()` wrapper on the first plugin registration so that
|
|
115
|
+
* [RNEventBridgePlugin.delegate] is populated before any triggerCampaign / invalidateCampaign
|
|
116
|
+
* calls arrive from JS.
|
|
117
|
+
*/
|
|
118
|
+
@ReactMethod
|
|
119
|
+
fun registerBridge() {
|
|
120
|
+
Digia.register(rnPlugin)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── setCurrentScreen ─────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
@ReactMethod
|
|
126
|
+
fun setCurrentScreen(name: String) {
|
|
127
|
+
Digia.setCurrentScreen(name)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── triggerCampaign ──────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Forwards a campaign payload to the native DigiaCEPDelegate.
|
|
134
|
+
*
|
|
135
|
+
* This is called by the JS DigiaDelegate.onCampaignTriggered() implementation when a JS CEP
|
|
136
|
+
* plugin (e.g. DigiaMoEngagePlugin) delivers a campaign. The delegate routes it into the
|
|
137
|
+
* Compose overlay for rendering.
|
|
138
|
+
*/
|
|
139
|
+
@ReactMethod
|
|
140
|
+
fun triggerCampaign(id: String, content: ReadableMap, cepContext: ReadableMap) {
|
|
141
|
+
val delegate = rnPlugin.delegate ?: return
|
|
142
|
+
delegate.onCampaignTriggered(
|
|
143
|
+
InAppPayload(
|
|
144
|
+
id = id,
|
|
145
|
+
content = content.toHashMap().toMap(),
|
|
146
|
+
cepContext = cepContext.toHashMap().toMap(),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Triggers a fetched Digia campaign directly by campaign id/key. */
|
|
152
|
+
@ReactMethod
|
|
153
|
+
fun triggerCampaignById(campaignId: String) {
|
|
154
|
+
Digia.triggerCampaign(campaignId)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── invalidateCampaign ───────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/** Forwards a campaign invalidation to the native DigiaCEPDelegate. */
|
|
160
|
+
@ReactMethod
|
|
161
|
+
fun invalidateCampaign(campaignId: String) {
|
|
162
|
+
rnPlugin.delegate?.onCampaignInvalidated(campaignId)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Anchor registration ──────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
@ReactMethod
|
|
168
|
+
fun registerAnchor(key: String, x: Int, y: Int, width: Int, height: Int) {
|
|
169
|
+
Digia.registerAnchor(key, x, y, width, height)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@ReactMethod
|
|
173
|
+
fun unregisterAnchor(key: String) {
|
|
174
|
+
Digia.unregisterAnchor(key)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@ReactMethod
|
|
178
|
+
fun getRegisteredComponents(promise: Promise) {
|
|
179
|
+
promise.resolve(Arguments.createArray())
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Internal: mount the Compose overlay host ─────────────────────────────
|
|
183
|
+
|
|
184
|
+
private fun mountDigiaHost() {
|
|
185
|
+
val activity =
|
|
186
|
+
reactContext.currentActivity
|
|
187
|
+
?: run {
|
|
188
|
+
android.util.Log.w(
|
|
189
|
+
"DigiaHost",
|
|
190
|
+
"[mountDigiaHost] no current activity — skipping"
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
val contentRoot = activity.window.decorView.findViewWithTag<DigiaHostView>(DIGIA_HOST_TAG)
|
|
196
|
+
if (contentRoot != null) {
|
|
197
|
+
android.util.Log.d(
|
|
198
|
+
"DigiaHost",
|
|
199
|
+
"[mountDigiaHost] already mounted (tag found) — skipping"
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
android.util.Log.d("DigiaHost", "[mountDigiaHost] mounting native overlay on DecorView")
|
|
205
|
+
|
|
206
|
+
// Mount on the DecorView (not content area) so the overlay covers the full screen
|
|
207
|
+
// including status bar and navigation bar. This also ensures the Compose Popup's
|
|
208
|
+
// canvas y=0 aligns with the absolute screen y=0, matching getLocationOnScreen()
|
|
209
|
+
// coordinates used by DigiaAnchorView.
|
|
210
|
+
val decorView =
|
|
211
|
+
activity.window.decorView as? android.view.ViewGroup
|
|
212
|
+
?: run {
|
|
213
|
+
android.util.Log.w(
|
|
214
|
+
"DigiaHost",
|
|
215
|
+
"[mountDigiaHost] decorView not a ViewGroup — skipping"
|
|
216
|
+
)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
val composeView =
|
|
221
|
+
DigiaHostView(activity).apply {
|
|
222
|
+
tag = DIGIA_HOST_TAG
|
|
223
|
+
if (activity is LifecycleOwner) setViewTreeLifecycleOwner(activity)
|
|
224
|
+
if (activity is ViewModelStoreOwner) setViewTreeViewModelStoreOwner(activity)
|
|
225
|
+
if (activity is SavedStateRegistryOwner)
|
|
226
|
+
setViewTreeSavedStateRegistryOwner(activity)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
decorView.addView(
|
|
230
|
+
composeView,
|
|
231
|
+
android.view.ViewGroup.LayoutParams(
|
|
232
|
+
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
|
|
233
|
+
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
android.util.Log.d(
|
|
237
|
+
"DigiaHost",
|
|
238
|
+
"[mountDigiaHost] done — DecorView child count: ${decorView.childCount}"
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
companion object {
|
|
243
|
+
const val MODULE_NAME = "DigiaEngageModule"
|
|
244
|
+
private const val DIGIA_HOST_TAG = "digia_host_compose_view"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* RNEventBridgePlugin
|
|
250
|
+
*
|
|
251
|
+
* The single native DigiaCEPPlugin used in React Native.
|
|
252
|
+
*
|
|
253
|
+
* When Digia.register(rnPlugin) is called, the SDK calls setup(delegate) which gives us the
|
|
254
|
+
* DigiaCEPDelegate reference. This delegate is used by triggerCampaign / invalidateCampaign bridge
|
|
255
|
+
* methods to push campaigns into the native rendering engine.
|
|
256
|
+
*
|
|
257
|
+
* Overlay lifecycle events (impressed / clicked / dismissed) received via notifyEvent() are emitted
|
|
258
|
+
* to JS as DeviceEventEmitter events so that JS CEP plugins can report analytics back to their
|
|
259
|
+
* platform.
|
|
260
|
+
*/
|
|
261
|
+
internal class RNEventBridgePlugin(
|
|
262
|
+
private val reactContext: ReactApplicationContext,
|
|
263
|
+
) : DigiaCEPPlugin {
|
|
264
|
+
|
|
265
|
+
override val identifier: String = "rn-event-bridge"
|
|
266
|
+
|
|
267
|
+
/** Delegate received from the SDK via setup(). Used by DigiaModule bridge methods. */
|
|
268
|
+
var delegate: DigiaCEPDelegate? = null
|
|
269
|
+
private set
|
|
270
|
+
|
|
271
|
+
private fun emit(event: String, params: com.facebook.react.bridge.WritableMap) {
|
|
272
|
+
if (reactContext.hasActiveReactInstance()) {
|
|
273
|
+
reactContext
|
|
274
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
275
|
+
.emit(event, params)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
override fun setup(delegate: DigiaCEPDelegate) {
|
|
280
|
+
this.delegate = delegate
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
override fun forwardScreen(name: String) {
|
|
284
|
+
/* forwarded by Digia.setCurrentScreen() on the native side */
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
override fun notifyEvent(event: DigiaExperienceEvent, payload: InAppPayload) {
|
|
288
|
+
val params =
|
|
289
|
+
Arguments.createMap().apply {
|
|
290
|
+
putString("campaignId", payload.id)
|
|
291
|
+
when (event) {
|
|
292
|
+
is DigiaExperienceEvent.Impressed -> putString("type", "impressed")
|
|
293
|
+
is DigiaExperienceEvent.Clicked -> {
|
|
294
|
+
putString("type", "clicked")
|
|
295
|
+
event.elementId?.let { putString("elementId", it) }
|
|
296
|
+
}
|
|
297
|
+
is DigiaExperienceEvent.Dismissed -> putString("type", "dismissed")
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
emit("digiaEngageEvent", params)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
override fun teardown() {
|
|
304
|
+
delegate = null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
override fun healthCheck(): DiagnosticReport =
|
|
308
|
+
DiagnosticReport(isHealthy = true, metadata = mapOf("identifier" to identifier))
|
|
309
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigiaPackage
|
|
3
|
+
*
|
|
4
|
+
* Registers the Digia Engage native module and view managers with React Native.
|
|
5
|
+
*
|
|
6
|
+
* Extends TurboReactPackage so the package works transparently on both Old Architecture (bridge)
|
|
7
|
+
* and New Architecture (TurboModules / JSI).
|
|
8
|
+
*
|
|
9
|
+
* Why is this needed? ──────────────────── React Native's auto-linker reads react-native.config.js,
|
|
10
|
+
* which declares `packageInstance: 'new DigiaPackage()'`. At host-app compile time, RN injects this
|
|
11
|
+
* into the generated `PackageList`, making our native module and view managers available to JS
|
|
12
|
+
* without any manual MainApplication edits.
|
|
13
|
+
*
|
|
14
|
+
* JS: NativeModules.DigiaEngageModule ──► DigiaModule.kt JS:
|
|
15
|
+
* requireNativeComponent('DigiaHostView') ──► DigiaViewManager.kt JS:
|
|
16
|
+
* requireNativeComponent('DigiaSlotView') ──► DigiaSlotViewManager.kt
|
|
17
|
+
*/
|
|
18
|
+
package com.digia.engage.rn
|
|
19
|
+
|
|
20
|
+
import com.facebook.react.BaseReactPackage
|
|
21
|
+
import com.facebook.react.bridge.NativeModule
|
|
22
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
23
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
24
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
25
|
+
import com.facebook.react.uimanager.ViewManager
|
|
26
|
+
|
|
27
|
+
class DigiaPackage : BaseReactPackage() {
|
|
28
|
+
|
|
29
|
+
// ─── Native Modules ───────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Called by New Architecture (TurboModules) to instantiate the module by name. Old Architecture
|
|
33
|
+
* continues to use createNativeModules() which TurboReactPackage implements internally by
|
|
34
|
+
* delegating to this method via getReactModuleInfoProvider.
|
|
35
|
+
*/
|
|
36
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
|
|
37
|
+
when (name) {
|
|
38
|
+
DigiaModule.MODULE_NAME -> DigiaModule(reactContext)
|
|
39
|
+
else -> null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Metadata table: tells the RN runtime which modules this package provides and whether they
|
|
44
|
+
* should be treated as TurboModules (New Architecture).
|
|
45
|
+
*/
|
|
46
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
|
|
47
|
+
mapOf(
|
|
48
|
+
DigiaModule.MODULE_NAME to
|
|
49
|
+
ReactModuleInfo(
|
|
50
|
+
/* name */ DigiaModule.MODULE_NAME,
|
|
51
|
+
/* className */ DigiaModule.MODULE_NAME,
|
|
52
|
+
/* canOverrideExistingModule */ false,
|
|
53
|
+
/* needsEagerInit */ false,
|
|
54
|
+
/* isCxxModule */ false,
|
|
55
|
+
/* isTurboModule */ false,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── View Managers ────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
override fun createViewManagers(
|
|
63
|
+
reactContext: ReactApplicationContext,
|
|
64
|
+
): List<ViewManager<*, *>> =
|
|
65
|
+
listOf(
|
|
66
|
+
DigiaViewManager(),
|
|
67
|
+
DigiaSlotViewManager(),
|
|
68
|
+
DigiaAnchorViewManager(),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
package com.digia.engage.rn
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewTreeObserver
|
|
6
|
+
import android.widget.FrameLayout
|
|
7
|
+
import androidx.lifecycle.LifecycleOwner
|
|
8
|
+
import androidx.lifecycle.ViewModelStoreOwner
|
|
9
|
+
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
10
|
+
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
|
11
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
12
|
+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
13
|
+
import com.digia.engage.DigiaSlotView
|
|
14
|
+
import com.facebook.react.bridge.Arguments
|
|
15
|
+
import com.facebook.react.bridge.WritableMap
|
|
16
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
17
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
18
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
19
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
20
|
+
import com.facebook.react.uimanager.events.Event
|
|
21
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
22
|
+
|
|
23
|
+
// ── ContentSizeChangeEvent ────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
private class ContentSizeChangeEvent(
|
|
26
|
+
surfaceId: Int,
|
|
27
|
+
viewTag: Int,
|
|
28
|
+
private val heightDp: Double,
|
|
29
|
+
private val widthDp: Double,
|
|
30
|
+
) : Event<ContentSizeChangeEvent>(surfaceId, viewTag) {
|
|
31
|
+
override fun getEventName(): String = "onContentSizeChange"
|
|
32
|
+
override fun getEventData(): WritableMap =
|
|
33
|
+
Arguments.createMap().apply {
|
|
34
|
+
putDouble("height", heightDp)
|
|
35
|
+
putDouble("width", widthDp)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── DigiaSlotContainerView ────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
// Plain FrameLayout wrapper: Fabric can measure it before window attachment.
|
|
42
|
+
// The inner DigiaSlotView (ComposeView) is created lazily in onAttachedToWindow.
|
|
43
|
+
internal class DigiaSlotContainerView(context: Context) : FrameLayout(context) {
|
|
44
|
+
|
|
45
|
+
var rnContext: ThemedReactContext? = null
|
|
46
|
+
|
|
47
|
+
private var _slotView: DigiaSlotView? = null
|
|
48
|
+
private val lastReportedHeightPx = AtomicInteger(-1)
|
|
49
|
+
|
|
50
|
+
var placementKey: String = ""
|
|
51
|
+
set(value) {
|
|
52
|
+
field = value
|
|
53
|
+
val slot = _slotView ?: return
|
|
54
|
+
slot.placementKey = value
|
|
55
|
+
lastReportedHeightPx.set(-1)
|
|
56
|
+
// Defer measure so the slot has a non-zero width. Do NOT call requestLayout() here —
|
|
57
|
+
// it sets PFLAG_FORCE_LAYOUT on the container and RN bypasses a full traversal,
|
|
58
|
+
// causing subsequent Compose requestLayout() calls to be swallowed.
|
|
59
|
+
post { measureAndDispatch() }
|
|
60
|
+
postDelayed({
|
|
61
|
+
lastReportedHeightPx.set(-1)
|
|
62
|
+
measureAndDispatch()
|
|
63
|
+
}, DELAYED_MEASURE_MS)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
67
|
+
measureAndDispatch()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// preDrawListener catches Compose content changes that globalLayoutListener misses in RN's
|
|
71
|
+
// layout model: Compose's invalidate() propagates to ViewRootImpl even when requestLayout()
|
|
72
|
+
// is blocked by PFLAG_FORCE_LAYOUT.
|
|
73
|
+
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
74
|
+
measureAndDispatch()
|
|
75
|
+
true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun onAttachedToWindow() {
|
|
79
|
+
super.onAttachedToWindow()
|
|
80
|
+
if (_slotView == null) createSlotView()
|
|
81
|
+
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
|
|
82
|
+
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
|
83
|
+
post { measureAndDispatch() }
|
|
84
|
+
// Retry: catches campaigns that arrive slightly after mount.
|
|
85
|
+
postDelayed({
|
|
86
|
+
if (lastReportedHeightPx.get() <= 0) {
|
|
87
|
+
lastReportedHeightPx.set(-1)
|
|
88
|
+
measureAndDispatch()
|
|
89
|
+
}
|
|
90
|
+
}, DELAYED_MEASURE_MS)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override fun onDetachedFromWindow() {
|
|
94
|
+
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
|
|
95
|
+
viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
96
|
+
super.onDetachedFromWindow()
|
|
97
|
+
lastReportedHeightPx.set(-1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Defer measureAndDispatch to avoid re-measuring during Compose's composition phase,
|
|
101
|
+
// which crashes with "pending composition has not been applied".
|
|
102
|
+
override fun requestLayout() {
|
|
103
|
+
super.requestLayout()
|
|
104
|
+
post { measureAndDispatch() }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun createSlotView() {
|
|
108
|
+
val themedCtx = context as? ThemedReactContext
|
|
109
|
+
val activityCtx: Context = themedCtx?.currentActivity ?: context
|
|
110
|
+
|
|
111
|
+
val slot = DigiaSlotView(activityCtx)
|
|
112
|
+
|
|
113
|
+
val activity = themedCtx?.currentActivity
|
|
114
|
+
if (activity is LifecycleOwner) slot.setViewTreeLifecycleOwner(activity)
|
|
115
|
+
if (activity is ViewModelStoreOwner) slot.setViewTreeViewModelStoreOwner(activity)
|
|
116
|
+
if (activity is SavedStateRegistryOwner) slot.setViewTreeSavedStateRegistryOwner(activity)
|
|
117
|
+
|
|
118
|
+
slot.placementKey = placementKey
|
|
119
|
+
slot.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
120
|
+
slot.addOnLayoutChangeListener { _: View, l: Int, _: Int, r: Int, _: Int,
|
|
121
|
+
_: Int, _: Int, _: Int, _: Int ->
|
|
122
|
+
if (r - l > 0) measureAndDispatch()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
addView(slot)
|
|
126
|
+
_slotView = slot
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fun measureAndDispatch() {
|
|
130
|
+
val slot = _slotView ?: return
|
|
131
|
+
val ctx = rnContext ?: return
|
|
132
|
+
val viewWidth = width
|
|
133
|
+
if (viewWidth <= 0) return
|
|
134
|
+
|
|
135
|
+
val widthSpec = MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY)
|
|
136
|
+
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
137
|
+
slot.measure(widthSpec, heightSpec)
|
|
138
|
+
val intrinsicHeightPx = slot.measuredHeight
|
|
139
|
+
|
|
140
|
+
if (intrinsicHeightPx == lastReportedHeightPx.get()) return
|
|
141
|
+
lastReportedHeightPx.set(intrinsicHeightPx)
|
|
142
|
+
|
|
143
|
+
val density = resources.displayMetrics.density
|
|
144
|
+
val heightDp = intrinsicHeightPx / density
|
|
145
|
+
|
|
146
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(ctx, id) ?: return
|
|
147
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
148
|
+
dispatcher.dispatchEvent(ContentSizeChangeEvent(surfaceId, id, heightDp.toDouble(), 0.0))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
companion object {
|
|
152
|
+
private const val DELAYED_MEASURE_MS = 300L
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── DigiaSlotViewManager ──────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
internal class DigiaSlotViewManager : SimpleViewManager<DigiaSlotContainerView>() {
|
|
159
|
+
|
|
160
|
+
override fun getName(): String = VIEW_NAME
|
|
161
|
+
|
|
162
|
+
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> =
|
|
163
|
+
mapOf("onContentSizeChange" to mapOf("registrationName" to "onContentSizeChange"))
|
|
164
|
+
|
|
165
|
+
override fun createViewInstance(context: ThemedReactContext): DigiaSlotContainerView {
|
|
166
|
+
val container = DigiaSlotContainerView(context)
|
|
167
|
+
container.rnContext = context
|
|
168
|
+
container.layoutParams = FrameLayout.LayoutParams(
|
|
169
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
170
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
171
|
+
)
|
|
172
|
+
return container
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@ReactProp(name = "placementKey")
|
|
176
|
+
fun setPlacementKey(view: DigiaSlotContainerView, placementKey: String?) {
|
|
177
|
+
view.placementKey = placementKey.orEmpty()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
companion object {
|
|
181
|
+
const val VIEW_NAME = "DigiaSlotView"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DigiaViewManager
|
|
3
|
+
*
|
|
4
|
+
* React Native ViewManager that exposes `DigiaHostView` (from the Digia Android SDK) as the native
|
|
5
|
+
* view behind the JS `<DigiaHostView>` component.
|
|
6
|
+
*
|
|
7
|
+
* The view hosts the `DigiaHost` composable which manages dialog and bottom-sheet overlays driven
|
|
8
|
+
* by Digia CEP plugins.
|
|
9
|
+
*/
|
|
10
|
+
package com.digia.engage.rn
|
|
11
|
+
|
|
12
|
+
import android.content.Context
|
|
13
|
+
import android.widget.FrameLayout
|
|
14
|
+
import androidx.lifecycle.LifecycleOwner
|
|
15
|
+
import androidx.lifecycle.ViewModelStoreOwner
|
|
16
|
+
import androidx.lifecycle.setViewTreeLifecycleOwner
|
|
17
|
+
import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
|
18
|
+
import androidx.savedstate.SavedStateRegistryOwner
|
|
19
|
+
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
20
|
+
import com.digia.engage.DigiaHostView
|
|
21
|
+
import com.facebook.react.uimanager.SimpleViewManager
|
|
22
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
23
|
+
|
|
24
|
+
internal class DigiaViewManager : SimpleViewManager<DigiaHostView>() {
|
|
25
|
+
|
|
26
|
+
override fun getName(): String = VIEW_NAME
|
|
27
|
+
|
|
28
|
+
override fun createViewInstance(context: ThemedReactContext): DigiaHostView {
|
|
29
|
+
// Prefer the current Activity as the context provider so that the Compose
|
|
30
|
+
// runtime can access a proper LifecycleOwner / ViewModelStoreOwner.
|
|
31
|
+
val activityContext: Context = context.currentActivity ?: context
|
|
32
|
+
|
|
33
|
+
val view = DigiaHostView(activityContext)
|
|
34
|
+
|
|
35
|
+
// Explicitly wire the Android Architecture Component owners so that the
|
|
36
|
+
// Compose runtime (which uses ViewTree* APIs) works correctly regardless
|
|
37
|
+
// of React Native's internal view hierarchy wiring.
|
|
38
|
+
val activity = context.currentActivity
|
|
39
|
+
if (activity is LifecycleOwner) {
|
|
40
|
+
view.setViewTreeLifecycleOwner(activity)
|
|
41
|
+
}
|
|
42
|
+
if (activity is ViewModelStoreOwner) {
|
|
43
|
+
view.setViewTreeViewModelStoreOwner(activity)
|
|
44
|
+
}
|
|
45
|
+
if (activity is SavedStateRegistryOwner) {
|
|
46
|
+
view.setViewTreeSavedStateRegistryOwner(activity)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// React Native controls sizing via its layout system; fill whatever space
|
|
50
|
+
// the JS stylesheet allocates (typically StyleSheet.absoluteFill).
|
|
51
|
+
view.layoutParams =
|
|
52
|
+
FrameLayout.LayoutParams(
|
|
53
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
54
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
android.util.Log.d("DigiaHost", "[DigiaViewManager] RN-side NativeDigiaHostView instance created (id=${view.id})")
|
|
58
|
+
return view
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
companion object {
|
|
62
|
+
const val VIEW_NAME = "DigiaHostView"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/android/build.gradle
CHANGED
|
@@ -71,7 +71,7 @@ android {
|
|
|
71
71
|
|
|
72
72
|
dependencies {
|
|
73
73
|
// Digia Engage Android library
|
|
74
|
-
implementation 'tech.digia:engage:2.
|
|
74
|
+
implementation 'tech.digia:engage:2.1.0'
|
|
75
75
|
|
|
76
76
|
// ── React Native ─────────────────────────────────────────────────────────
|
|
77
77
|
// React Native is provided by the host app; mark as compileOnly so it is
|