@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
@@ -1,21 +1,37 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'DigiaEngageReactNative'
3
3
  s.version = '0.1.0'
4
- s.summary = 'React Native bridge for the Digia Engage SDK (Android Compose UI).'
4
+ s.summary = 'React Native bridge for the Digia Engage SDK (iOS & Android).'
5
5
  s.description = <<-DESC
6
- Provides a React Native bridge that surfaces the Digia Engage Android
7
- Compose UI SDK inside React Native applications. iOS support is stubbed
8
- and will be added in a future release.
6
+ Provides a React Native bridge that surfaces the Digia Engage SDK inside
7
+ React Native applications. Supports both iOS (SwiftUI) and Android
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
- s.ios.deployment_target = '13.0'
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'
17
22
 
18
23
  s.source_files = 'ios/**/*.{h,m,mm,swift}'
19
24
 
25
+ # Swift version must match the Digia iOS SDK.
26
+ s.swift_version = '5.9'
27
+
20
28
  s.dependency 'React-Core'
29
+
30
+ # ── Digia Engage iOS SDK ──────────────────────────────────────────────────
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
+
35
+ # ── New Architecture (Fabric / TurboModules) support ─────────────────────
36
+ install_modules_dependencies(s)
21
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 (Android)
161
+ ### 3 – Open the Digia UI navigation flow
162
162
 
163
- Launch the full-screen Compose navigation activity managed by the Digia DSL
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
- | `openNavigation` | `(options?: DigiaNavigationOptions) => void` | Launch the full-screen Digia UI navigation activity. |
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
- ### `DigiaNavigationOptions`
223
+ ### `CreateInitialPageOptions`
230
224
 
231
- | Prop | Type | Description |
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
- │ ├── NativeDigiaModule.ts ← Low-level native module binding
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, openNavigation)
252
+ │ ├── DigiaModule.kt ← NativeModule (initialize, setCurrentScreen, createInitialPage)
262
253
  │ ├── DigiaViewManager.kt ← ViewManager for <DigiaHostView>
263
254
  │ └── DigiaHostComposeView.kt ← AbstractComposeView hosting DigiaHost { }
264
255
 
@@ -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>
@@ -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.03'
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
@@ -17,9 +17,7 @@ dependencyResolutionManagement {
17
17
  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18
18
  repositories {
19
19
  google()
20
- mavenCentral()
21
- mavenLocal()
22
- maven { url 'https://jitpack.io' }
20
+ mavenCentral()
23
21
  }
24
22
  }
25
23
 
@@ -222,7 +222,7 @@ internal class RNEventBridgePlugin(
222
222
  is DigiaExperienceEvent.Dismissed -> putString("type", "dismissed")
223
223
  }
224
224
  }
225
- emit("digiaOverlayEvent", params)
225
+ emit("digiaEngageEvent", params)
226
226
  }
227
227
 
228
228
  override fun teardown() {
@@ -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
- internal class DigiaSlotViewManager : SimpleViewManager<DigiaSlotView>() {
23
+ // ── ContentSizeChangeEvent ────────────────────────────────────────────────────
26
24
 
27
- override fun getName(): String = VIEW_NAME
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
- override fun createViewInstance(context: ThemedReactContext): DigiaSlotView {
30
- val activityContext: Context = context.currentActivity ?: context
35
+ // ── DigiaSlotContainerView ────────────────────────────────────────────────────
31
36
 
32
- val view = DigiaSlotView(activityContext)
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
- // Wire Architecture Component owners so the Compose runtime works correctly.
35
- val activity = context.currentActivity
36
- if (activity is LifecycleOwner) {
37
- view.setViewTreeLifecycleOwner(activity)
38
- }
39
- if (activity is ViewModelStoreOwner) {
40
- view.setViewTreeViewModelStoreOwner(activity)
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
- if (activity is SavedStateRegistryOwner) {
43
- view.setViewTreeSavedStateRegistryOwner(activity)
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
- // React Native controls sizing; fill whatever space the JS stylesheet allocates.
47
- view.layoutParams =
48
- FrameLayout.LayoutParams(
49
- FrameLayout.LayoutParams.MATCH_PARENT,
50
- FrameLayout.LayoutParams.MATCH_PARENT,
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
- return view
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: DigiaSlotView, placementKey: String?) {
172
+ fun setPlacementKey(view: DigiaSlotContainerView, placementKey: String?) {
58
173
  view.placementKey = placementKey.orEmpty()
59
174
  }
60
175
 
@@ -1,71 +1,52 @@
1
1
  /**
2
- * DigiaEngageModule (iOS stub)
2
+ * DigiaEngageModule.m
3
3
  *
4
- * iOS support is not yet implemented. This stub registers the module so that
5
- * the JS layer does not crash when imported on iOS. All methods are no-ops.
4
+ * ObjC bridge file that exports Swift implementations to the React Native
5
+ * runtime (both Old Architecture bridge and New Architecture TurboModules).
6
+ *
7
+ * All real logic lives in the Swift files alongside this one:
8
+ * DigiaModule.swift — NativeModule (RCTEventEmitter subclass)
9
+ * RNEventBridgePlugin.swift — DigiaCEPPlugin bridge
10
+ * DigiaHostViewManager.swift — ViewManager for <DigiaHostView>
11
+ * DigiaSlotViewManager.swift — ViewManager for <DigiaSlotView>
6
12
  */
13
+
7
14
  #import <React/RCTBridgeModule.h>
15
+ #import <React/RCTEventEmitter.h>
8
16
  #import <React/RCTViewManager.h>
9
17
 
10
- // ── NativeModule stub ─────────────────────────────────────────────────────────
11
-
12
- @interface DigiaEngageModule : NSObject <RCTBridgeModule>
13
- @end
18
+ // ── NativeModule ──────────────────────────────────────────────────────────────
14
19
 
15
- @implementation DigiaEngageModule
20
+ // RCT_EXTERN_MODULE wires the Swift class DigiaModule (which inherits
21
+ // RCTEventEmitter) to the React Native bridge under the name "DigiaEngageModule".
16
22
 
17
- RCT_EXPORT_MODULE(DigiaEngageModule)
23
+ @interface RCT_EXTERN_MODULE(DigiaEngageModule, RCTEventEmitter)
18
24
 
19
- RCT_EXPORT_METHOD(initialize:(NSString *)apiKey
25
+ RCT_EXTERN_METHOD(initialize:(NSString *)apiKey
20
26
  environment:(NSString *)environment
21
27
  logLevel:(NSString *)logLevel
22
28
  resolve:(RCTPromiseResolveBlock)resolve
23
29
  reject:(RCTPromiseRejectBlock)reject)
24
- {
25
- // iOS not yet implemented – resolve immediately so JS doesn't hang.
26
- resolve(nil);
27
- }
28
30
 
29
- RCT_EXPORT_METHOD(setCurrentScreen:(NSString *)name)
30
- {
31
- // no-op
32
- }
31
+ RCT_EXTERN_METHOD(registerBridge)
33
32
 
34
- RCT_EXPORT_METHOD(openNavigation:(nullable NSString *)startPageId
35
- pageArgs:(NSDictionary *)pageArgs)
36
- {
37
- // no-op
38
- }
33
+ RCT_EXTERN_METHOD(setCurrentScreen:(NSString *)name)
39
34
 
40
- RCT_EXPORT_METHOD(triggerCampaign:(NSString *)campaignId
35
+ RCT_EXTERN_METHOD(triggerCampaign:(NSString *)id
41
36
  content:(NSDictionary *)content
42
37
  cepContext:(NSDictionary *)cepContext)
43
- {
44
- // no-op — iOS Digia SDK not yet available
45
- }
46
38
 
47
- RCT_EXPORT_METHOD(invalidateCampaign:(NSString *)campaignId)
48
- {
49
- // no-op
50
- }
39
+ RCT_EXTERN_METHOD(invalidateCampaign:(NSString *)campaignId)
51
40
 
52
41
  @end
53
42
 
54
43
 
55
- // ── ViewManager stub ──────────────────────────────────────────────────────────
44
+ // ── ViewManagers ──────────────────────────────────────────────────────────────
56
45
 
57
- @interface DigiaHostViewManager : RCTViewManager
46
+ @interface RCT_EXTERN_MODULE(DigiaHostView, RCTViewManager)
58
47
  @end
59
48
 
60
- @implementation DigiaHostViewManager
61
-
62
- RCT_EXPORT_MODULE(DigiaHostView)
63
-
64
- - (UIView *)view {
65
- // Return a transparent placeholder view
66
- UIView *v = [[UIView alloc] init];
67
- v.userInteractionEnabled = NO;
68
- return v;
69
- }
70
-
49
+ @interface RCT_EXTERN_MODULE(DigiaSlotView, RCTViewManager)
50
+ RCT_EXPORT_VIEW_PROPERTY(placementKey, NSString)
51
+ RCT_EXPORT_VIEW_PROPERTY(onContentSizeChange, RCTDirectEventBlock)
71
52
  @end
@@ -0,0 +1,128 @@
1
+ /**
2
+ * DigiaHostViewManager
3
+ *
4
+ * React Native ViewManager that exposes a UIView wrapping DigiaHost (from the
5
+ * Digia iOS SDK) as the native view behind the JS <DigiaHostView> component.
6
+ *
7
+ * DigiaHost is a SwiftUI view, so we bridge it into UIKit using a
8
+ * UIHostingController embedded as a child view controller. The host view
9
+ * manages dialog and bottom-sheet overlays driven by Digia CEP plugins.
10
+ *
11
+ * Place <DigiaHostView> once at the root of your RN component tree.
12
+ */
13
+ import SwiftUI
14
+ import React
15
+ import DigiaEngage
16
+
17
+ @objc(DigiaHostView)
18
+ final class DigiaHostViewManager: RCTViewManager {
19
+
20
+ override static func requiresMainQueueSetup() -> Bool { true }
21
+
22
+ override func view() -> UIView! {
23
+ return DigiaHostUIView()
24
+ }
25
+ }
26
+
27
+ // MARK: - DigiaHostUIView
28
+
29
+ /// Lightweight UIView container that embeds a UIHostingController<DigiaHost>
30
+ /// as a child so SwiftUI's DigiaHost composable renders overlays above all
31
+ /// React Native content.
32
+ final class DigiaHostUIView: UIView {
33
+
34
+ private var hostingController: UIHostingController<DigiaHostWrapperView>?
35
+
36
+ override func didMoveToWindow() {
37
+ super.didMoveToWindow()
38
+ guard window != nil else {
39
+ // View removed from hierarchy — tear down the hosting controller.
40
+ hostingController?.willMove(toParent: nil)
41
+ hostingController?.view.removeFromSuperview()
42
+ hostingController?.removeFromParent()
43
+ hostingController = nil
44
+ return
45
+ }
46
+ guard hostingController == nil else { return }
47
+ mountHostingController()
48
+ }
49
+
50
+ private func mountHostingController() {
51
+ guard let parentVC = parentViewController() else { return}
52
+
53
+ let swiftUIView = DigiaHostWrapperView()
54
+ let hc = UIHostingController(rootView: swiftUIView)
55
+ hc.view.translatesAutoresizingMaskIntoConstraints = false
56
+ hc.view.backgroundColor = .clear
57
+
58
+ parentVC.addChild(hc)
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)
68
+ hc.didMove(toParent: parentVC)
69
+
70
+ NSLayoutConstraint.activate([
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),
75
+ ])
76
+
77
+ hostingController = hc
78
+ }
79
+
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`.
90
+ private func parentViewController() -> UIViewController? {
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
107
+ }
108
+ return nil
109
+ }
110
+ }
111
+
112
+ // MARK: - SwiftUI wrapper
113
+
114
+ /// A SwiftUI view that acts as the DigiaHost root. An EmptyView is used as
115
+ /// content because React Native's own navigation already manages the app's
116
+ /// view hierarchy — DigiaHost only needs to be mounted to activate the overlay
117
+ /// layer.
118
+ struct DigiaHostWrapperView: View {
119
+ var body: some View {
120
+ DigiaHost {
121
+ EmptyView()
122
+ }
123
+ // No frame constraint here — the view fills its parent (absoluteFill
124
+ // from JS). A non-zero frame is required so UIKit calls viewDidAppear
125
+ // on the UIHostingController, which in turn triggers SwiftUI onAppear
126
+ // and establishes the onChange(activePayload) subscription.
127
+ }
128
+ }