@digia-engage/core 2.3.2 → 2.4.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.
Files changed (62) hide show
  1. package/DigiaEngageReactNative.podspec +1 -1
  2. package/android/.project +28 -0
  3. package/android/build.gradle +1 -1
  4. package/android/local.properties +1 -0
  5. package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +64 -6
  6. package/ios/DigiaModule.swift +70 -8
  7. package/ios/RNEventBridgePlugin.swift +2 -0
  8. package/lib/commonjs/Digia.js +139 -100
  9. package/lib/commonjs/Digia.js.map +1 -1
  10. package/lib/commonjs/DigiaAnchorView.js +11 -1
  11. package/lib/commonjs/DigiaAnchorView.js.map +1 -1
  12. package/lib/commonjs/DigiaProvider.js +63 -2
  13. package/lib/commonjs/DigiaProvider.js.map +1 -1
  14. package/lib/commonjs/NativeDigiaEngage.js +3 -0
  15. package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
  16. package/lib/commonjs/digiaAnchorRegistry.js +3 -1
  17. package/lib/commonjs/digiaAnchorRegistry.js.map +1 -1
  18. package/lib/commonjs/frequencyStore.js +3 -3
  19. package/lib/commonjs/frequencyStore.js.map +1 -1
  20. package/lib/commonjs/index.js +0 -7
  21. package/lib/commonjs/index.js.map +1 -1
  22. package/lib/module/Digia.js +139 -100
  23. package/lib/module/Digia.js.map +1 -1
  24. package/lib/module/DigiaAnchorView.js +11 -1
  25. package/lib/module/DigiaAnchorView.js.map +1 -1
  26. package/lib/module/DigiaProvider.js +65 -3
  27. package/lib/module/DigiaProvider.js.map +1 -1
  28. package/lib/module/NativeDigiaEngage.js +3 -0
  29. package/lib/module/NativeDigiaEngage.js.map +1 -1
  30. package/lib/module/digiaAnchorRegistry.js +3 -1
  31. package/lib/module/digiaAnchorRegistry.js.map +1 -1
  32. package/lib/module/frequencyStore.js +3 -3
  33. package/lib/module/frequencyStore.js.map +1 -1
  34. package/lib/module/index.js +4 -4
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/typescript/Digia.d.ts +17 -8
  37. package/lib/typescript/Digia.d.ts.map +1 -1
  38. package/lib/typescript/DigiaAnchorView.d.ts.map +1 -1
  39. package/lib/typescript/DigiaProvider.d.ts +3 -1
  40. package/lib/typescript/DigiaProvider.d.ts.map +1 -1
  41. package/lib/typescript/NativeDigiaEngage.d.ts +9 -0
  42. package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
  43. package/lib/typescript/digiaAnchorRegistry.d.ts +1 -0
  44. package/lib/typescript/digiaAnchorRegistry.d.ts.map +1 -1
  45. package/lib/typescript/frequencyStore.d.ts +1 -1
  46. package/lib/typescript/frequencyStore.d.ts.map +1 -1
  47. package/lib/typescript/index.d.ts +5 -5
  48. package/lib/typescript/index.d.ts.map +1 -1
  49. package/lib/typescript/templateTypes.d.ts +24 -1
  50. package/lib/typescript/templateTypes.d.ts.map +1 -1
  51. package/lib/typescript/types.d.ts +17 -13
  52. package/lib/typescript/types.d.ts.map +1 -1
  53. package/package.json +8 -9
  54. package/src/Digia.ts +142 -114
  55. package/src/DigiaAnchorView.tsx +6 -1
  56. package/src/DigiaProvider.tsx +76 -2
  57. package/src/NativeDigiaEngage.ts +20 -0
  58. package/src/digiaAnchorRegistry.ts +3 -1
  59. package/src/frequencyStore.ts +4 -4
  60. package/src/index.ts +5 -5
  61. package/src/templateTypes.ts +31 -1
  62. package/src/types.ts +17 -13
@@ -3,17 +3,27 @@ import {
3
3
  Animated,
4
4
  Dimensions,
5
5
  Modal,
6
+ Platform,
6
7
  Pressable,
7
8
  StyleSheet,
8
9
  Text,
9
10
  View,
11
+ requireNativeComponent,
10
12
  useWindowDimensions,
11
13
  } from 'react-native';
14
+
15
+ // Native view that hosts Digia's Compose overlay (dialogs / bottom sheets).
16
+ // pointerEvents="none" ensures it never intercepts touches.
17
+ const NativeDigiaHostView =
18
+ Platform.OS === 'android' || Platform.OS === 'ios'
19
+ ? requireNativeComponent<{ style?: object; pointerEvents?: string }>('DigiaHostView')
20
+ : null;
12
21
  import { computePosition, flip, offset, shift } from '@floating-ui/core';
13
22
  import Svg, { Path } from 'react-native-svg';
14
23
  import { Digia } from './Digia';
15
24
  import { digiaGuideController, type DigiaGuideRequest } from './DigiaGuideController';
16
25
  import { digiaAnchorRegistry, type AnchorLayout } from './digiaAnchorRegistry';
26
+ import { digiaHealthReporter, HealthEventType } from './DigiaHealthReporter';
17
27
  import { digiaActionHandler, type ActionCallbacks } from './actionHandler';
18
28
  import type { DismissReason } from './types';
19
29
  import type { Action, SpotlightConfig, SpotlightStep, TooltipConfig, TooltipStep } from './templateTypes';
@@ -247,9 +257,25 @@ function TooltipOverlay({
247
257
  setLayout(null);
248
258
  setFloatPos(null);
249
259
  if (!ready) return;
260
+ if (!digiaAnchorRegistry.isRegistered(step.anchorKey)) {
261
+ // eslint-disable-next-line no-console
262
+ console.warn(`[Digia] campaign dropped — anchor_key "${step.anchorKey}" is not registered on this screen (campaign_key=${request.campaignKey}, step=${stepIndex})`);
263
+ digiaHealthReporter.report(HealthEventType.anchor_not_on_screen, {
264
+ campaign_key: request.campaignKey,
265
+ reason: 'anchor_key_not_registered',
266
+ anchor_key: step.anchorKey,
267
+ step_index: stepIndex,
268
+ });
269
+ digiaGuideController.cancel(request.payloadId);
270
+ return;
271
+ }
250
272
  let skipCached = false;
251
273
  const unsub = digiaAnchorRegistry.subscribe(step.anchorKey, (l) => {
252
274
  if (!skipCached) return;
275
+ if (l.width === 0 || l.height === 0) {
276
+ digiaGuideController.cancel(request.payloadId);
277
+ return;
278
+ }
253
279
  const { width: screenW, height: screenH } = Dimensions.get('window');
254
280
  if (l.pageY + l.height <= 0 || l.pageY >= screenH || l.pageX + l.width <= 0 || l.pageX >= screenW) {
255
281
  digiaGuideController.cancel(request.payloadId);
@@ -632,9 +658,25 @@ function SpotlightOverlay({
632
658
  useEffect(() => {
633
659
  setLayout(null);
634
660
  if (!ready) return; // hold off measuring/showing until the delay has elapsed
661
+ if (!digiaAnchorRegistry.isRegistered(step.anchorKey)) {
662
+ // eslint-disable-next-line no-console
663
+ console.warn(`[Digia] campaign dropped — anchor_key "${step.anchorKey}" is not registered on this screen (campaign_key=${request.campaignKey}, step=${stepIndex})`);
664
+ digiaHealthReporter.report(HealthEventType.anchor_not_on_screen, {
665
+ campaign_key: request.campaignKey,
666
+ reason: 'anchor_key_not_registered',
667
+ anchor_key: step.anchorKey,
668
+ step_index: stepIndex,
669
+ });
670
+ digiaGuideController.cancel(request.payloadId);
671
+ return;
672
+ }
635
673
  let skipCached = false;
636
674
  const unsub = digiaAnchorRegistry.subscribe(step.anchorKey, (l) => {
637
675
  if (!skipCached) return;
676
+ if (l.width === 0 || l.height === 0) {
677
+ digiaGuideController.cancel(request.payloadId);
678
+ return;
679
+ }
638
680
  const { width: screenW, height: screenH } = Dimensions.get('window');
639
681
  if (l.pageY + l.height <= 0 || l.pageY >= screenH || l.pageX + l.width <= 0 || l.pageX >= screenW) {
640
682
  digiaGuideController.cancel(request.payloadId);
@@ -811,9 +853,41 @@ function DigiaGuideRuntime() {
811
853
  }
812
854
 
813
855
  // ─── DigiaHost ────────────────────────────────────────────────────────────────
856
+ //
857
+ // Place once at the app root. Accepts optional children (wrap mode) or can be
858
+ // used standalone (<DigiaHost />) as a sibling alongside other root elements.
859
+ //
860
+ // Renders two overlays, both non-interactive (pointerEvents="none"):
861
+ // 1. JS guide / tooltip / spotlight renderer (DigiaGuideRuntime)
862
+ // 2. Native Compose overlay for dialogs / bottom sheets (DigiaHostView)
863
+
864
+ export function DigiaHost({ children }: { children?: React.ReactNode }) {
865
+ const overlay = (
866
+ <>
867
+ <DigiaGuideRuntime />
868
+ {NativeDigiaHostView && (
869
+ // The outer View with pointerEvents="none" is critical: RN's touch
870
+ // dispatch respects pointerEvents on plain Views even in bridgeless
871
+ // mode. Applying it directly on requireNativeComponent views is
872
+ // unreliable in New Architecture. The Compose dialogs/sheets inside
873
+ // render in their own Android PhoneWindow so they stay interactive.
874
+ <View style={StyleSheet.absoluteFillObject} pointerEvents="none">
875
+ <NativeDigiaHostView style={StyleSheet.absoluteFillObject} />
876
+ </View>
877
+ )}
878
+ </>
879
+ );
880
+
881
+ if (children != null) {
882
+ return (
883
+ <>
884
+ {children}
885
+ {overlay}
886
+ </>
887
+ );
888
+ }
814
889
 
815
- export function DigiaHost() {
816
- return <DigiaGuideRuntime />;
890
+ return overlay;
817
891
  }
818
892
 
819
893
  // ─── Styles ───────────────────────────────────────────────────────────────────
@@ -55,6 +55,22 @@ export interface Spec extends TurboModule {
55
55
 
56
56
  /** Return all registered components (anchors/slots) for health reporting. */
57
57
  getRegisteredComponents(): Promise<Array<{ component_key: string; component_type: 'anchor' | 'slot'; screen_name: string | null }>>;
58
+
59
+ /** Set the authenticated user ID for analytics identity stitching. */
60
+ setUserId(userId: string): void;
61
+ /** Clear the user ID (e.g. on logout); rotates the analytics session. */
62
+ clearUserId(): void;
63
+ /**
64
+ * Record an analytics event from a JS-rendered campaign (guide/tooltip/spotlight).
65
+ * Native campaigns (nudge, inline, survey) are tracked automatically by the SDK.
66
+ */
67
+ trackEvent(
68
+ eventType: string,
69
+ campaignId: string,
70
+ campaignKey: string,
71
+ campaignType: string,
72
+ elementId?: string | null,
73
+ ): void;
58
74
  }
59
75
 
60
76
  let _resolved: Spec | null = null;
@@ -89,5 +105,9 @@ export const nativeDigiaModule: Spec = {
89
105
  registerAnchor: (key, x, y, width, height) => getModule()?.registerAnchor(key, x, y, width, height),
90
106
  unregisterAnchor: (key) => getModule()?.unregisterAnchor(key),
91
107
  getRegisteredComponents: () => getModule()?.getRegisteredComponents() ?? Promise.resolve([]),
108
+ setUserId: (userId) => getModule()?.setUserId(userId),
109
+ clearUserId: () => getModule()?.clearUserId(),
110
+ trackEvent: (eventType, campaignId, campaignKey, campaignType, elementId) =>
111
+ getModule()?.trackEvent(eventType, campaignId, campaignKey, campaignType, elementId),
92
112
  getConstants: () => getModule()?.getConstants?.() ?? {},
93
113
  };
@@ -38,5 +38,7 @@ const remeasure = (key: string) => {
38
38
  _measureCallbacks.get(key)?.()
39
39
  }
40
40
 
41
+ const isRegistered = (key: string): boolean => _measureCallbacks.has(key)
42
+
41
43
  export type { AnchorLayout }
42
- export const digiaAnchorRegistry = { setLayout, getLayout, subscribe, remove, registerMeasure, unregisterMeasure, remeasure }
44
+ export const digiaAnchorRegistry = { setLayout, getLayout, subscribe, remove, registerMeasure, unregisterMeasure, remeasure, isRegistered }
@@ -36,18 +36,18 @@ const _sessionStore = new Map<string, FrequencyState>();
36
36
  const storeKey = (campaignKey: string) => `digia:freq:${campaignKey}`;
37
37
 
38
38
  export const frequencyStore = {
39
- async checkProjectId(projectId: string): Promise<void> {
39
+ async checkApiKey(apiKey: string): Promise<void> {
40
40
  const storage = getStorage();
41
41
  if (!storage) return;
42
42
  try {
43
43
  const stored = await storage.getItem(STORE_META_KEY);
44
- const meta = stored ? (JSON.parse(stored) as { projectId: string }) : null;
45
- if (meta && meta.projectId !== projectId) {
44
+ const meta = stored ? (JSON.parse(stored) as { apiKey: string }) : null;
45
+ if (meta && meta.apiKey !== apiKey) {
46
46
  const keys = await storage.getAllKeys();
47
47
  const digiaKeys = keys.filter((k) => k.startsWith('digia:freq:'));
48
48
  if (digiaKeys.length > 0) await storage.multiRemove([...digiaKeys]);
49
49
  }
50
- await storage.setItem(STORE_META_KEY, JSON.stringify({ projectId }));
50
+ await storage.setItem(STORE_META_KEY, JSON.stringify({ apiKey }));
51
51
  } catch {
52
52
  // non-fatal
53
53
  }
package/src/index.ts CHANGED
@@ -4,18 +4,18 @@
4
4
  * React Native bridge for the Digia Engage SDK.
5
5
  *
6
6
  * The SDK surfaces Digia Compose UI inside React Native via:
7
- * • `Digia` – SDK lifecycle (initialize, setCurrentScreen)
8
- * • `DigiaHostView` – Transparent native overlay view that hosts Compose dialogs/
9
- * bottom-sheets managed by the Digia CEP engine.
7
+ * • `Digia` – SDK lifecycle (initialize, setCurrentScreen)
8
+ * • `DigiaHost` – Place once at the app root. Hosts JS guide/tooltip overlays
9
+ * and the native Compose overlay for dialogs/bottom-sheets.
10
+ * Use as <DigiaHost /> standalone or <DigiaHost>{children}</DigiaHost>.
10
11
  */
11
12
 
12
13
  export { Digia } from './Digia';
13
- export { DigiaHostView } from './DigiaHostView';
14
14
  export { DigiaHost } from './DigiaProvider';
15
15
  export { DigiaSlotView } from './DigiaSlotView';
16
16
  export { DigiaAnchorView } from './DigiaAnchorView';
17
17
  export type { DigiaAnchorViewRef } from './DigiaAnchorView';
18
- export type { ActionContext, ActionResult, CampaignType, DigiaAction, DigiaConfig, DigiaDelegate, DigiaExperienceEvent, DigiaPlugin, InAppBrowserAdapter, InAppPayload, OnAction } from './types';
18
+ export type { ActionContext, ActionResult, CEPTriggerPayload, CampaignType, DigiaAction, DigiaConfig, DigiaDelegate, DigiaExperienceEvent, DigiaPlugin, InAppBrowserAdapter, OnAction } from './types';
19
19
  export { defaultInAppBrowser } from './defaultInAppBrowser';
20
20
  export { DigiaHealthReporter, HealthEventType, digiaHealthReporter } from './DigiaHealthReporter';
21
21
  export type {
@@ -120,4 +120,34 @@ export type SurveyTemplateConfig = {
120
120
  rootNodeId: string
121
121
  }
122
122
 
123
- export type TemplateConfig = TooltipConfig | SpotlightConfig | CarouselConfig | SurveyTemplateConfig
123
+ export type NudgeContainerConfig = {
124
+ bgColor?: string
125
+ cornerRadius?: number
126
+ padding?: number
127
+ dismissOnOutsideTap?: boolean
128
+ scrimColor?: string
129
+ /** Bottom-sheet only: max height as a fraction of screen height. */
130
+ maxHeightRatio?: number
131
+ /** Bottom-sheet only: show the drag handle + enable drag-to-dismiss. */
132
+ dragHandle?: boolean
133
+ /** Dialog only: width in dp. */
134
+ width?: number
135
+ }
136
+
137
+ /**
138
+ * BottomSheet / dialog nudge. `layout` is the native DUI VWData tree (root
139
+ * `digia/column`), parsed and rendered entirely by the native SDK — JS does not
140
+ * render nudges; they are forwarded to the native bridge via triggerCampaign.
141
+ */
142
+ export type NudgeConfig = {
143
+ templateType: 'bottomSheet' | 'dialog'
144
+ container: NudgeContainerConfig
145
+ layout: Record<string, unknown>
146
+ }
147
+
148
+ export type TemplateConfig =
149
+ | TooltipConfig
150
+ | SpotlightConfig
151
+ | CarouselConfig
152
+ | SurveyTemplateConfig
153
+ | NudgeConfig
package/src/types.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  /**
2
- * Payload delivered to the Digia rendering engine for a CEP campaign.
2
+ * The translation contract between a CEP plugin and Digia's rendering engine.
3
3
  *
4
- * Mirrors InAppPayload on Android / Flutter.
4
+ * Plugin authors map their CEP's native callback into this struct.
5
+ * Mirrors CEPTriggerPayload on Android / Flutter — Digia core never imports
6
+ * CleverTap, MoEngage, or WebEngage types directly.
5
7
  */
6
- export interface InAppPayload {
7
- /** Unique campaign ID from the CEP platform. */
8
- id: string;
9
- /** Marketer-authored content map (JSON-serialisable). */
10
- content: Record<string, unknown>;
11
- /** CEP-platform metadata, e.g. { campaignId, campaignName }. */
12
- cepContext: Record<string, unknown>;
8
+ export interface CEPTriggerPayload {
9
+ /** The CEP's own identifier for this campaign instance. Opaque to Digia — passed through for analytics correlation. */
10
+ cepCampaignId: string;
11
+ /** Additional metadata the CEP passes through (UTM params, user segment, CEP-specific tracking fields). Forwarded as-is in ExperienceEvents. */
12
+ cepMetadata: Record<string, unknown>;
13
+ /** The coupling key linking this CEP campaign to a Digia campaign. Used to look up the matching campaign in the store. */
14
+ campaignKey: string;
15
+ /** Optional runtime variables to interpolate into the campaign config. Keys must match variable placeholders in the Digia dashboard. */
16
+ variables?: Record<string, string>;
13
17
  }
14
18
 
15
19
  export type CampaignType = 'nudge' | 'guide' | 'inline' | 'survey';
@@ -70,7 +74,7 @@ export type GuideLifecycleEvent =
70
74
  */
71
75
  export interface DigiaDelegate {
72
76
  /** Deliver a campaign payload into the Digia rendering engine. */
73
- onCampaignTriggered(payload: InAppPayload): void | Promise<void>;
77
+ onCampaignTriggered(payload: CEPTriggerPayload): void | Promise<void>;
74
78
  /** Invalidate / dismiss a campaign by its ID. */
75
79
  onCampaignInvalidated(campaignId: string): void;
76
80
  }
@@ -90,7 +94,7 @@ export interface DigiaPlugin {
90
94
  * (impressed / clicked / dismissed). Plugins use this to report
91
95
  * analytics back to their CEP platform.
92
96
  */
93
- notifyEvent(event: DigiaExperienceEvent, payload: InAppPayload): void;
97
+ notifyEvent(event: DigiaExperienceEvent, payload: CEPTriggerPayload): void;
94
98
  /**
95
99
  * Called by the Digia SDK to record a named analytics event with properties.
96
100
  * Implement this to forward Digia lifecycle events (e.g. "Digia Experience Viewed")
@@ -170,8 +174,8 @@ export interface FrequencyEvalResult {
170
174
  * Configuration for initialising the Digia Engage SDK.
171
175
  */
172
176
  export interface DigiaConfig {
173
- /** The Engage project ID — sent as x-digia-project-id on all SDK requests. */
174
- projectId: string;
177
+ /** The Engage API key — sent as x-digia-project-id on all SDK requests. */
178
+ apiKey: string;
175
179
  /**
176
180
  * Base URL for the Digia API.
177
181
  * Defaults to the production API root, or the Engage sandbox root when