@digia-engage/core 1.0.0-beta.5 → 1.0.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 +12 -15
- package/README.md +8 -17
- package/android/.project +28 -0
- package/android/build.gradle +1 -1
- package/android/settings.gradle +1 -2
- 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 +1 -0
- package/ios/DigiaHostViewManager.swift +41 -17
- package/ios/DigiaModule.swift +55 -6
- package/ios/DigiaSlotViewManager.swift +190 -22
- package/ios/RNEventBridgePlugin.swift +10 -11
- package/lib/commonjs/Digia.js +50 -0
- package/lib/commonjs/Digia.js.map +1 -1
- package/lib/commonjs/DigiaHostView.js +4 -50
- package/lib/commonjs/DigiaHostView.js.map +1 -1
- package/lib/commonjs/DigiaSlotView.js +35 -53
- 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 +4 -51
- package/lib/module/DigiaHostView.js.map +1 -1
- package/lib/module/DigiaSlotView.js +35 -51
- 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 -29
- package/lib/typescript/DigiaHostView.d.ts.map +1 -1
- package/lib/typescript/DigiaSlotView.d.ts +3 -40
- 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 +5 -48
- package/src/DigiaSlotView.tsx +40 -48
- package/src/NativeDigiaEngage.ts +1 -0
- package/src/index.ts +1 -1
- package/src/types.ts +30 -0
|
@@ -8,13 +8,17 @@ Pod::Spec.new do |s|
|
|
|
8
8
|
(Jetpack Compose) using the New Architecture (TurboModules / Fabric).
|
|
9
9
|
DESC
|
|
10
10
|
|
|
11
|
-
s.homepage = 'https://github.com/Digia-Technology-Private-Limited/digia_engage'
|
|
12
11
|
s.license = { :type => 'MIT', :file => '../LICENSE' }
|
|
13
|
-
s.author = { 'Digia Technology Private Limited' => '' }
|
|
14
|
-
s.source = { :git => 'https://github.com/Digia-Technology-Private-Limited/digia_engage.git', :tag => s.version.to_s }
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
s.
|
|
13
|
+
s.authors = { 'Digia Technology Private Limited' => 'https://digia.tech' }
|
|
14
|
+
s.homepage = 'https://github.com/Digia-Technology-Private-Limited/digia_engage'
|
|
15
|
+
s.source = {
|
|
16
|
+
:git => 'https://github.com/Digia-Technology-Private-Limited/digia_engage.git',
|
|
17
|
+
:tag => "react-native-v#{s.version}"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# DigiaEngage iOS SDK requires iOS 17+ (SwiftUI features used internally).
|
|
21
|
+
s.ios.deployment_target = '17.0'
|
|
18
22
|
|
|
19
23
|
s.source_files = 'ios/**/*.{h,m,mm,swift}'
|
|
20
24
|
|
|
@@ -24,17 +28,10 @@ Pod::Spec.new do |s|
|
|
|
24
28
|
s.dependency 'React-Core'
|
|
25
29
|
|
|
26
30
|
# ── Digia Engage iOS SDK ──────────────────────────────────────────────────
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
# When integrating via CocoaPods, add the git source to your Podfile:
|
|
31
|
-
# pod 'DigiaEngage', :git => 'https://github.com/Digia-Technology-Private-Limited/digia_engage_iOS.git',
|
|
32
|
-
# :tag => '1.0.0-beta.1'
|
|
33
|
-
s.dependency 'DigiaEngage', '1.0.0-beta.1'
|
|
31
|
+
# Available on SPM: https://swiftpackageindex.com/Digia-Technology-Private-Limited/digia_engage_iOS
|
|
32
|
+
# CocoaPods: host app Podfile must declare the git source (see README).
|
|
33
|
+
s.dependency 'DigiaEngage'
|
|
34
34
|
|
|
35
35
|
# ── New Architecture (Fabric / TurboModules) support ─────────────────────
|
|
36
|
-
# install_modules_dependencies wires the pod into both Old Architecture
|
|
37
|
-
# (bridge) and New Architecture (JSI / Fabric) automatically when the host
|
|
38
|
-
# app has `use_frameworks!` / `use_react_native!` with :fabric_enabled.
|
|
39
36
|
install_modules_dependencies(s)
|
|
40
37
|
end
|
package/README.md
CHANGED
|
@@ -158,10 +158,9 @@ const navRef = useNavigationContainerRef();
|
|
|
158
158
|
>
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
### 3 – Open the Digia UI navigation flow
|
|
161
|
+
### 3 – Open the Digia UI navigation flow
|
|
162
162
|
|
|
163
|
-
Launch the full-screen
|
|
164
|
-
configuration:
|
|
163
|
+
Launch the full-screen native SDUI stack:
|
|
165
164
|
|
|
166
165
|
```tsx
|
|
167
166
|
import { Digia } from '@digia/engage-react-native';
|
|
@@ -170,12 +169,7 @@ function MyScreen() {
|
|
|
170
169
|
return (
|
|
171
170
|
<Button
|
|
172
171
|
title="Open Digia Experience"
|
|
173
|
-
onPress={() =>
|
|
174
|
-
Digia.openNavigation({
|
|
175
|
-
startPageId: 'onboarding',
|
|
176
|
-
pageArgs: { userId: '123' },
|
|
177
|
-
})
|
|
178
|
-
}
|
|
172
|
+
onPress={() => Digia.createInitialPage()}
|
|
179
173
|
/>
|
|
180
174
|
);
|
|
181
175
|
}
|
|
@@ -216,7 +210,7 @@ const styles = StyleSheet.create({ root: { flex: 1 } });
|
|
|
216
210
|
|---|---|---|
|
|
217
211
|
| `initialize` | `(config: DigiaConfig) => Promise<void>` | Initialise the SDK and mount the Compose overlay host. |
|
|
218
212
|
| `setCurrentScreen` | `(name: string) => void` | Notify the SDK of the current screen. |
|
|
219
|
-
| `
|
|
213
|
+
| `createInitialPage` | `() => void` | Full-screen Digia SDUI (Android: `DigiaUINavigationActivity`; iOS: modal `DigiaNavigationView`). |
|
|
220
214
|
|
|
221
215
|
### `DigiaConfig`
|
|
222
216
|
|
|
@@ -226,12 +220,9 @@ const styles = StyleSheet.create({ root: { flex: 1 } });
|
|
|
226
220
|
| `environment` | `'production' \| 'sandbox'` | `'production'` | Target environment. |
|
|
227
221
|
| `logLevel` | `'none' \| 'error' \| 'verbose'` | `'error'` | Log verbosity. |
|
|
228
222
|
|
|
229
|
-
### `
|
|
223
|
+
### `CreateInitialPageOptions`
|
|
230
224
|
|
|
231
|
-
|
|
232
|
-
|---|---|---|
|
|
233
|
-
| `startPageId` | `string?` | DSL page ID to start from. |
|
|
234
|
-
| `pageArgs` | `Record<string, string>?` | Key/value args forwarded to the start page. |
|
|
225
|
+
Empty interface — reserved for future optional arguments.
|
|
235
226
|
|
|
236
227
|
### `<DigiaHostView>`
|
|
237
228
|
|
|
@@ -251,14 +242,14 @@ react-native/
|
|
|
251
242
|
│ ├── index.ts ← Public API exports
|
|
252
243
|
│ ├── types.ts ← TypeScript interfaces
|
|
253
244
|
│ ├── Digia.ts ← High-level JS SDK wrapper
|
|
254
|
-
│ ├──
|
|
245
|
+
│ ├── NativeDigiaEngage.ts ← Low-level native module binding
|
|
255
246
|
│ └── DigiaHostView.tsx ← <DigiaHostView> React component
|
|
256
247
|
│
|
|
257
248
|
├── android/
|
|
258
249
|
│ ├── build.gradle ← Android library build config
|
|
259
250
|
│ └── src/main/java/com/digia/engage/rn/
|
|
260
251
|
│ ├── DigiaPackage.kt ← ReactPackage (registers module + view)
|
|
261
|
-
│ ├── DigiaModule.kt ← NativeModule (initialize, setCurrentScreen,
|
|
252
|
+
│ ├── DigiaModule.kt ← NativeModule (initialize, setCurrentScreen, createInitialPage)
|
|
262
253
|
│ ├── DigiaViewManager.kt ← ViewManager for <DigiaHostView>
|
|
263
254
|
│ └── DigiaHostComposeView.kt ← AbstractComposeView hosting DigiaHost { }
|
|
264
255
|
│
|
package/android/.project
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<projectDescription>
|
|
3
|
+
<name>digia-engage_core</name>
|
|
4
|
+
<comment>Project digia-engage_core created by Buildship.</comment>
|
|
5
|
+
<projects>
|
|
6
|
+
</projects>
|
|
7
|
+
<buildSpec>
|
|
8
|
+
<buildCommand>
|
|
9
|
+
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
|
|
10
|
+
<arguments>
|
|
11
|
+
</arguments>
|
|
12
|
+
</buildCommand>
|
|
13
|
+
</buildSpec>
|
|
14
|
+
<natures>
|
|
15
|
+
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
|
|
16
|
+
</natures>
|
|
17
|
+
<filteredResources>
|
|
18
|
+
<filter>
|
|
19
|
+
<id>1775559486270</id>
|
|
20
|
+
<name></name>
|
|
21
|
+
<type>30</type>
|
|
22
|
+
<matcher>
|
|
23
|
+
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
|
24
|
+
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
|
25
|
+
</matcher>
|
|
26
|
+
</filter>
|
|
27
|
+
</filteredResources>
|
|
28
|
+
</projectDescription>
|
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:1.0.0-beta.
|
|
74
|
+
implementation 'tech.digia:engage:1.0.0-beta.04'
|
|
75
75
|
|
|
76
76
|
// ── React Native ─────────────────────────────────────────────────────────
|
|
77
77
|
// React Native is provided by the host app; mark as compileOnly so it is
|
package/android/settings.gradle
CHANGED
|
@@ -17,8 +17,7 @@ dependencyResolutionManagement {
|
|
|
17
17
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
18
18
|
repositories {
|
|
19
19
|
google()
|
|
20
|
-
mavenCentral()
|
|
21
|
-
maven { url 'https://jitpack.io' }
|
|
20
|
+
mavenCentral()
|
|
22
21
|
}
|
|
23
22
|
}
|
|
24
23
|
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DigiaSlotViewManager
|
|
3
|
-
*
|
|
4
|
-
* React Native ViewManager that exposes [DigiaSlotView] (from the Digia Android SDK) as the native
|
|
5
|
-
* view behind the JS `<DigiaSlotView>` component.
|
|
6
|
-
*
|
|
7
|
-
* Supported JS props:
|
|
8
|
-
* - `placementKey` (String) — matches the placement key set in the Digia dashboard.
|
|
9
|
-
*/
|
|
10
1
|
package com.digia.engage.rn
|
|
11
2
|
|
|
12
3
|
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewTreeObserver
|
|
13
6
|
import android.widget.FrameLayout
|
|
14
7
|
import androidx.lifecycle.LifecycleOwner
|
|
15
8
|
import androidx.lifecycle.ViewModelStoreOwner
|
|
@@ -18,43 +11,165 @@ import androidx.lifecycle.setViewTreeViewModelStoreOwner
|
|
|
18
11
|
import androidx.savedstate.SavedStateRegistryOwner
|
|
19
12
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
|
20
13
|
import com.digia.engage.DigiaSlotView
|
|
14
|
+
import com.facebook.react.bridge.Arguments
|
|
15
|
+
import com.facebook.react.bridge.WritableMap
|
|
21
16
|
import com.facebook.react.uimanager.SimpleViewManager
|
|
22
17
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
18
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
23
19
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
20
|
+
import com.facebook.react.uimanager.events.Event
|
|
21
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
// ── ContentSizeChangeEvent ────────────────────────────────────────────────────
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
private class ContentSizeChangeEvent(
|
|
26
|
+
surfaceId: Int,
|
|
27
|
+
viewTag: Int,
|
|
28
|
+
private val heightDp: Double,
|
|
29
|
+
) : Event<ContentSizeChangeEvent>(surfaceId, viewTag) {
|
|
30
|
+
override fun getEventName(): String = "onContentSizeChange"
|
|
31
|
+
override fun getEventData(): WritableMap =
|
|
32
|
+
Arguments.createMap().apply { putDouble("height", heightDp) }
|
|
33
|
+
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
val activityContext: Context = context.currentActivity ?: context
|
|
35
|
+
// ── DigiaSlotContainerView ────────────────────────────────────────────────────
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
// Plain FrameLayout wrapper: Fabric can measure it before window attachment.
|
|
38
|
+
// The inner DigiaSlotView (ComposeView) is created lazily in onAttachedToWindow.
|
|
39
|
+
internal class DigiaSlotContainerView(context: Context) : FrameLayout(context) {
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
var rnContext: ThemedReactContext? = null
|
|
42
|
+
|
|
43
|
+
private var _slotView: DigiaSlotView? = null
|
|
44
|
+
private val lastReportedHeightPx = AtomicInteger(-1)
|
|
45
|
+
|
|
46
|
+
var placementKey: String = ""
|
|
47
|
+
set(value) {
|
|
48
|
+
field = value
|
|
49
|
+
val slot = _slotView ?: return
|
|
50
|
+
slot.placementKey = value
|
|
51
|
+
lastReportedHeightPx.set(-1)
|
|
52
|
+
// Defer measure so the slot has a non-zero width. Do NOT call requestLayout() here —
|
|
53
|
+
// it sets PFLAG_FORCE_LAYOUT on the container and RN bypasses a full traversal,
|
|
54
|
+
// causing subsequent Compose requestLayout() calls to be swallowed.
|
|
55
|
+
post { measureAndDispatch() }
|
|
56
|
+
postDelayed({
|
|
57
|
+
lastReportedHeightPx.set(-1)
|
|
58
|
+
measureAndDispatch()
|
|
59
|
+
}, DELAYED_MEASURE_MS)
|
|
41
60
|
}
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
|
|
62
|
+
private val globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
|
|
63
|
+
measureAndDispatch()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// preDrawListener catches Compose content changes that globalLayoutListener misses in RN's
|
|
67
|
+
// layout model: Compose's invalidate() propagates to ViewRootImpl even when requestLayout()
|
|
68
|
+
// is blocked by PFLAG_FORCE_LAYOUT.
|
|
69
|
+
private val preDrawListener = ViewTreeObserver.OnPreDrawListener {
|
|
70
|
+
measureAndDispatch()
|
|
71
|
+
true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override fun onAttachedToWindow() {
|
|
75
|
+
super.onAttachedToWindow()
|
|
76
|
+
if (_slotView == null) createSlotView()
|
|
77
|
+
viewTreeObserver.addOnGlobalLayoutListener(globalLayoutListener)
|
|
78
|
+
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
|
79
|
+
post { measureAndDispatch() }
|
|
80
|
+
// Retry: catches campaigns that arrive slightly after mount.
|
|
81
|
+
postDelayed({
|
|
82
|
+
if (lastReportedHeightPx.get() <= 0) {
|
|
83
|
+
lastReportedHeightPx.set(-1)
|
|
84
|
+
measureAndDispatch()
|
|
85
|
+
}
|
|
86
|
+
}, DELAYED_MEASURE_MS)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
override fun onDetachedFromWindow() {
|
|
90
|
+
viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutListener)
|
|
91
|
+
viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
|
92
|
+
super.onDetachedFromWindow()
|
|
93
|
+
lastReportedHeightPx.set(-1)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Defer measureAndDispatch to avoid re-measuring during Compose's composition phase,
|
|
97
|
+
// which crashes with "pending composition has not been applied".
|
|
98
|
+
override fun requestLayout() {
|
|
99
|
+
super.requestLayout()
|
|
100
|
+
post { measureAndDispatch() }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private fun createSlotView() {
|
|
104
|
+
val themedCtx = context as? ThemedReactContext
|
|
105
|
+
val activityCtx: Context = themedCtx?.currentActivity ?: context
|
|
106
|
+
|
|
107
|
+
val slot = DigiaSlotView(activityCtx)
|
|
108
|
+
|
|
109
|
+
val activity = themedCtx?.currentActivity
|
|
110
|
+
if (activity is LifecycleOwner) slot.setViewTreeLifecycleOwner(activity)
|
|
111
|
+
if (activity is ViewModelStoreOwner) slot.setViewTreeViewModelStoreOwner(activity)
|
|
112
|
+
if (activity is SavedStateRegistryOwner) slot.setViewTreeSavedStateRegistryOwner(activity)
|
|
113
|
+
|
|
114
|
+
slot.placementKey = placementKey
|
|
115
|
+
slot.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
116
|
+
slot.addOnLayoutChangeListener { _: View, l: Int, _: Int, r: Int, _: Int,
|
|
117
|
+
_: Int, _: Int, _: Int, _: Int ->
|
|
118
|
+
if (r - l > 0) measureAndDispatch()
|
|
44
119
|
}
|
|
45
120
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
121
|
+
addView(slot)
|
|
122
|
+
_slotView = slot
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
fun measureAndDispatch() {
|
|
126
|
+
val slot = _slotView ?: return
|
|
127
|
+
val ctx = rnContext ?: return
|
|
128
|
+
val viewWidth = width
|
|
129
|
+
if (viewWidth <= 0) return
|
|
130
|
+
|
|
131
|
+
val widthSpec = MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY)
|
|
132
|
+
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
133
|
+
slot.measure(widthSpec, heightSpec)
|
|
134
|
+
val intrinsicHeightPx = slot.measuredHeight
|
|
135
|
+
|
|
136
|
+
if (intrinsicHeightPx == lastReportedHeightPx.get()) return
|
|
137
|
+
lastReportedHeightPx.set(intrinsicHeightPx)
|
|
138
|
+
|
|
139
|
+
val density = resources.displayMetrics.density
|
|
140
|
+
val heightDp = intrinsicHeightPx / density
|
|
141
|
+
|
|
142
|
+
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(ctx, id) ?: return
|
|
143
|
+
val surfaceId = UIManagerHelper.getSurfaceId(this)
|
|
144
|
+
dispatcher.dispatchEvent(ContentSizeChangeEvent(surfaceId, id, heightDp.toDouble()))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
companion object {
|
|
148
|
+
private const val DELAYED_MEASURE_MS = 300L
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── DigiaSlotViewManager ──────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
internal class DigiaSlotViewManager : SimpleViewManager<DigiaSlotContainerView>() {
|
|
155
|
+
|
|
156
|
+
override fun getName(): String = VIEW_NAME
|
|
157
|
+
|
|
158
|
+
override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> =
|
|
159
|
+
mapOf("onContentSizeChange" to mapOf("registrationName" to "onContentSizeChange"))
|
|
52
160
|
|
|
53
|
-
|
|
161
|
+
override fun createViewInstance(context: ThemedReactContext): DigiaSlotContainerView {
|
|
162
|
+
val container = DigiaSlotContainerView(context)
|
|
163
|
+
container.rnContext = context
|
|
164
|
+
container.layoutParams = FrameLayout.LayoutParams(
|
|
165
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
166
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
167
|
+
)
|
|
168
|
+
return container
|
|
54
169
|
}
|
|
55
170
|
|
|
56
171
|
@ReactProp(name = "placementKey")
|
|
57
|
-
fun setPlacementKey(view:
|
|
172
|
+
fun setPlacementKey(view: DigiaSlotContainerView, placementKey: String?) {
|
|
58
173
|
view.placementKey = placementKey.orEmpty()
|
|
59
174
|
}
|
|
60
175
|
|
package/ios/DigiaEngageModule.m
CHANGED
|
@@ -48,38 +48,62 @@ final class DigiaHostUIView: UIView {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
private func mountHostingController() {
|
|
51
|
-
guard let parentVC = parentViewController() else { return
|
|
51
|
+
guard let parentVC = parentViewController() else { return}
|
|
52
52
|
|
|
53
53
|
let swiftUIView = DigiaHostWrapperView()
|
|
54
54
|
let hc = UIHostingController(rootView: swiftUIView)
|
|
55
55
|
hc.view.translatesAutoresizingMaskIntoConstraints = false
|
|
56
56
|
hc.view.backgroundColor = .clear
|
|
57
|
-
// Disable touch interception so all taps pass through to React Native
|
|
58
|
-
// content below. The dialog/bottom-sheet overlays are presented as
|
|
59
|
-
// separate UIViewControllers (via ViewControllerUtil.present) so they
|
|
60
|
-
// independently capture touches when visible.
|
|
61
|
-
hc.view.isUserInteractionEnabled = false
|
|
62
57
|
|
|
63
58
|
parentVC.addChild(hc)
|
|
64
|
-
|
|
59
|
+
// Mount onto parentVC.view (full-screen) rather than self, because
|
|
60
|
+
// DigiaHostView is intentionally sized 0×0 in React Native (it takes no
|
|
61
|
+
// screen space). A zero-size UIHostingController frame prevents SwiftUI
|
|
62
|
+
// from rendering its body, which means @ObservedObject subscriptions and
|
|
63
|
+
// .onChange(of:) modifiers are never established — so activePayload
|
|
64
|
+
// changes are silently dropped and no overlay is ever shown.
|
|
65
|
+
// Anchoring to parentVC.view guarantees a non-zero frame so SwiftUI's
|
|
66
|
+
// rendering loop runs and reacts to SDK state changes.
|
|
67
|
+
parentVC.view.addSubview(hc.view)
|
|
65
68
|
hc.didMove(toParent: parentVC)
|
|
66
69
|
|
|
67
70
|
NSLayoutConstraint.activate([
|
|
68
|
-
hc.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
69
|
-
hc.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
70
|
-
hc.view.topAnchor.constraint(equalTo: topAnchor),
|
|
71
|
-
hc.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
71
|
+
hc.view.leadingAnchor.constraint(equalTo: parentVC.view.leadingAnchor),
|
|
72
|
+
hc.view.trailingAnchor.constraint(equalTo: parentVC.view.trailingAnchor),
|
|
73
|
+
hc.view.topAnchor.constraint(equalTo: parentVC.view.topAnchor),
|
|
74
|
+
hc.view.bottomAnchor.constraint(equalTo: parentVC.view.bottomAnchor),
|
|
72
75
|
])
|
|
73
76
|
|
|
74
77
|
hostingController = hc
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
//
|
|
80
|
+
// Pass touches through to RN when no overlay is active.
|
|
81
|
+
// When an overlay renders in-host (bottom sheet / dialog inside DigiaHost's ZStack),
|
|
82
|
+
// SwiftUI's hit test returns the overlay view and we forward that — making the
|
|
83
|
+
// overlay fully interactive. When nothing is rendered (EmptyView), SwiftUI returns
|
|
84
|
+
// nil and we return nil, so UIKit falls through to RN content below.
|
|
85
|
+
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
86
|
+
return hostingController?.view.hitTest(point, with: event)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Prefer React Native’s `UIView.reactViewController`, then walk `next` from each view’s `.next`.
|
|
78
90
|
private func parentViewController() -> UIViewController? {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
let reactSel = NSSelectorFromString("reactViewController")
|
|
92
|
+
var view: UIView? = self
|
|
93
|
+
while let v = view {
|
|
94
|
+
if v.responds(to: reactSel), let raw = v.perform(reactSel)?.takeUnretainedValue() {
|
|
95
|
+
if let vc = raw as? UIViewController { return vc }
|
|
96
|
+
}
|
|
97
|
+
view = v.superview
|
|
98
|
+
}
|
|
99
|
+
view = self
|
|
100
|
+
while let v = view {
|
|
101
|
+
var r: UIResponder? = v.next
|
|
102
|
+
while let responder = r {
|
|
103
|
+
if let vc = responder as? UIViewController { return vc }
|
|
104
|
+
r = responder.next
|
|
105
|
+
}
|
|
106
|
+
view = v.superview
|
|
83
107
|
}
|
|
84
108
|
return nil
|
|
85
109
|
}
|
|
@@ -91,7 +115,7 @@ final class DigiaHostUIView: UIView {
|
|
|
91
115
|
/// content because React Native's own navigation already manages the app's
|
|
92
116
|
/// view hierarchy — DigiaHost only needs to be mounted to activate the overlay
|
|
93
117
|
/// layer.
|
|
94
|
-
|
|
118
|
+
struct DigiaHostWrapperView: View {
|
|
95
119
|
var body: some View {
|
|
96
120
|
DigiaHost {
|
|
97
121
|
EmptyView()
|
package/ios/DigiaModule.swift
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* setCurrentScreen(name): void
|
|
10
10
|
* triggerCampaign(id, content, cepContext): void
|
|
11
11
|
* invalidateCampaign(campaignId): void
|
|
12
|
+
* createInitialPage(): void // AppConfig initial route
|
|
12
13
|
*
|
|
13
14
|
* Architecture
|
|
14
15
|
* ────────────
|
|
@@ -27,6 +28,8 @@
|
|
|
27
28
|
*/
|
|
28
29
|
import Foundation
|
|
29
30
|
import React
|
|
31
|
+
import SwiftUI
|
|
32
|
+
import UIKit
|
|
30
33
|
import DigiaEngage
|
|
31
34
|
|
|
32
35
|
@objc(DigiaEngageModule)
|
|
@@ -41,12 +44,16 @@ final class DigiaModule: RCTEventEmitter {
|
|
|
41
44
|
|
|
42
45
|
override static func requiresMainQueueSetup() -> Bool { true }
|
|
43
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
|
+
|
|
44
55
|
override func supportedEvents() -> [String]! {
|
|
45
|
-
return [
|
|
46
|
-
"digia_experience_impressed",
|
|
47
|
-
"digia_experience_clicked",
|
|
48
|
-
"digia_experience_dismissed",
|
|
49
|
-
]
|
|
56
|
+
return ["digiaEngageEvent"]
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
// ────────────────────────────────────────────────────────────────────────
|
|
@@ -77,6 +84,7 @@ final class DigiaModule: RCTEventEmitter {
|
|
|
77
84
|
Task { @MainActor in
|
|
78
85
|
do {
|
|
79
86
|
try await Digia.initialize(config)
|
|
87
|
+
self.mountDigiaHost()
|
|
80
88
|
resolve(nil)
|
|
81
89
|
} catch {
|
|
82
90
|
reject("DIGIA_INIT_ERROR", error.localizedDescription, error)
|
|
@@ -143,17 +151,58 @@ final class DigiaModule: RCTEventEmitter {
|
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
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
|
+
|
|
146
192
|
// ────────────────────────────────────────────────────────────────────────
|
|
147
193
|
// MARK: - Private helpers
|
|
148
194
|
|
|
149
195
|
private func buildInAppPayloadContent(from map: NSDictionary) -> InAppPayloadContent {
|
|
150
|
-
let type = (map["type"] as? String) ?? "dialog"
|
|
151
196
|
let pk = map["placementKey"] as? String
|
|
152
197
|
let title = map["title"] as? String
|
|
153
198
|
let text = map["text"] as? String
|
|
154
199
|
let viewId = map["viewId"] as? String
|
|
155
200
|
let command = map["command"] as? String
|
|
156
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
|
+
}
|
|
157
206
|
let args: [String: JSONValue] = {
|
|
158
207
|
guard let raw = map["args"] as? [String: Any] else { return [:] }
|
|
159
208
|
return raw.compactMapValues { JSONValue(rawValue: $0) }
|