@digia-engage/core 2.3.2 → 2.5.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 (63) 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/DigiaEngageModule.m +8 -0
  7. package/ios/DigiaModule.swift +117 -8
  8. package/ios/RNEventBridgePlugin.swift +2 -0
  9. package/lib/commonjs/Digia.js +139 -100
  10. package/lib/commonjs/Digia.js.map +1 -1
  11. package/lib/commonjs/DigiaAnchorView.js +11 -1
  12. package/lib/commonjs/DigiaAnchorView.js.map +1 -1
  13. package/lib/commonjs/DigiaProvider.js +50 -1
  14. package/lib/commonjs/DigiaProvider.js.map +1 -1
  15. package/lib/commonjs/NativeDigiaEngage.js +3 -0
  16. package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
  17. package/lib/commonjs/digiaAnchorRegistry.js +3 -1
  18. package/lib/commonjs/digiaAnchorRegistry.js.map +1 -1
  19. package/lib/commonjs/frequencyStore.js +3 -3
  20. package/lib/commonjs/frequencyStore.js.map +1 -1
  21. package/lib/commonjs/index.js +0 -7
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/module/Digia.js +139 -100
  24. package/lib/module/Digia.js.map +1 -1
  25. package/lib/module/DigiaAnchorView.js +11 -1
  26. package/lib/module/DigiaAnchorView.js.map +1 -1
  27. package/lib/module/DigiaProvider.js +50 -1
  28. package/lib/module/DigiaProvider.js.map +1 -1
  29. package/lib/module/NativeDigiaEngage.js +3 -0
  30. package/lib/module/NativeDigiaEngage.js.map +1 -1
  31. package/lib/module/digiaAnchorRegistry.js +3 -1
  32. package/lib/module/digiaAnchorRegistry.js.map +1 -1
  33. package/lib/module/frequencyStore.js +3 -3
  34. package/lib/module/frequencyStore.js.map +1 -1
  35. package/lib/module/index.js +4 -4
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/typescript/Digia.d.ts +17 -8
  38. package/lib/typescript/Digia.d.ts.map +1 -1
  39. package/lib/typescript/DigiaAnchorView.d.ts.map +1 -1
  40. package/lib/typescript/DigiaProvider.d.ts +3 -1
  41. package/lib/typescript/DigiaProvider.d.ts.map +1 -1
  42. package/lib/typescript/NativeDigiaEngage.d.ts +9 -0
  43. package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
  44. package/lib/typescript/digiaAnchorRegistry.d.ts +1 -0
  45. package/lib/typescript/digiaAnchorRegistry.d.ts.map +1 -1
  46. package/lib/typescript/frequencyStore.d.ts +1 -1
  47. package/lib/typescript/frequencyStore.d.ts.map +1 -1
  48. package/lib/typescript/index.d.ts +5 -5
  49. package/lib/typescript/index.d.ts.map +1 -1
  50. package/lib/typescript/templateTypes.d.ts +24 -1
  51. package/lib/typescript/templateTypes.d.ts.map +1 -1
  52. package/lib/typescript/types.d.ts +17 -13
  53. package/lib/typescript/types.d.ts.map +1 -1
  54. package/package.json +8 -9
  55. package/src/Digia.ts +142 -114
  56. package/src/DigiaAnchorView.tsx +6 -1
  57. package/src/DigiaProvider.tsx +56 -1
  58. package/src/NativeDigiaEngage.ts +20 -0
  59. package/src/digiaAnchorRegistry.ts +3 -1
  60. package/src/frequencyStore.ts +4 -4
  61. package/src/index.ts +5 -5
  62. package/src/templateTypes.ts +31 -1
  63. package/src/types.ts +17 -13
@@ -3,17 +3,20 @@ import {
3
3
  Animated,
4
4
  Dimensions,
5
5
  Modal,
6
+ Platform,
6
7
  Pressable,
7
8
  StyleSheet,
8
9
  Text,
9
10
  View,
10
11
  useWindowDimensions,
11
12
  } from 'react-native';
13
+
12
14
  import { computePosition, flip, offset, shift } from '@floating-ui/core';
13
15
  import Svg, { Path } from 'react-native-svg';
14
16
  import { Digia } from './Digia';
15
17
  import { digiaGuideController, type DigiaGuideRequest } from './DigiaGuideController';
16
18
  import { digiaAnchorRegistry, type AnchorLayout } from './digiaAnchorRegistry';
19
+ import { digiaHealthReporter, HealthEventType } from './DigiaHealthReporter';
17
20
  import { digiaActionHandler, type ActionCallbacks } from './actionHandler';
18
21
  import type { DismissReason } from './types';
19
22
  import type { Action, SpotlightConfig, SpotlightStep, TooltipConfig, TooltipStep } from './templateTypes';
@@ -247,9 +250,25 @@ function TooltipOverlay({
247
250
  setLayout(null);
248
251
  setFloatPos(null);
249
252
  if (!ready) return;
253
+ if (!digiaAnchorRegistry.isRegistered(step.anchorKey)) {
254
+ // eslint-disable-next-line no-console
255
+ console.warn(`[Digia] campaign dropped — anchor_key "${step.anchorKey}" is not registered on this screen (campaign_key=${request.campaignKey}, step=${stepIndex})`);
256
+ digiaHealthReporter.report(HealthEventType.anchor_not_on_screen, {
257
+ campaign_key: request.campaignKey,
258
+ reason: 'anchor_key_not_registered',
259
+ anchor_key: step.anchorKey,
260
+ step_index: stepIndex,
261
+ });
262
+ digiaGuideController.cancel(request.payloadId);
263
+ return;
264
+ }
250
265
  let skipCached = false;
251
266
  const unsub = digiaAnchorRegistry.subscribe(step.anchorKey, (l) => {
252
267
  if (!skipCached) return;
268
+ if (l.width === 0 || l.height === 0) {
269
+ digiaGuideController.cancel(request.payloadId);
270
+ return;
271
+ }
253
272
  const { width: screenW, height: screenH } = Dimensions.get('window');
254
273
  if (l.pageY + l.height <= 0 || l.pageY >= screenH || l.pageX + l.width <= 0 || l.pageX >= screenW) {
255
274
  digiaGuideController.cancel(request.payloadId);
@@ -632,9 +651,25 @@ function SpotlightOverlay({
632
651
  useEffect(() => {
633
652
  setLayout(null);
634
653
  if (!ready) return; // hold off measuring/showing until the delay has elapsed
654
+ if (!digiaAnchorRegistry.isRegistered(step.anchorKey)) {
655
+ // eslint-disable-next-line no-console
656
+ console.warn(`[Digia] campaign dropped — anchor_key "${step.anchorKey}" is not registered on this screen (campaign_key=${request.campaignKey}, step=${stepIndex})`);
657
+ digiaHealthReporter.report(HealthEventType.anchor_not_on_screen, {
658
+ campaign_key: request.campaignKey,
659
+ reason: 'anchor_key_not_registered',
660
+ anchor_key: step.anchorKey,
661
+ step_index: stepIndex,
662
+ });
663
+ digiaGuideController.cancel(request.payloadId);
664
+ return;
665
+ }
635
666
  let skipCached = false;
636
667
  const unsub = digiaAnchorRegistry.subscribe(step.anchorKey, (l) => {
637
668
  if (!skipCached) return;
669
+ if (l.width === 0 || l.height === 0) {
670
+ digiaGuideController.cancel(request.payloadId);
671
+ return;
672
+ }
638
673
  const { width: screenW, height: screenH } = Dimensions.get('window');
639
674
  if (l.pageY + l.height <= 0 || l.pageY >= screenH || l.pageX + l.width <= 0 || l.pageX >= screenW) {
640
675
  digiaGuideController.cancel(request.payloadId);
@@ -811,8 +846,28 @@ function DigiaGuideRuntime() {
811
846
  }
812
847
 
813
848
  // ─── DigiaHost ────────────────────────────────────────────────────────────────
849
+ //
850
+ // Place once at the app root. Accepts optional children (wrap mode) or can be
851
+ // used standalone (<DigiaHost />) as a sibling alongside other root elements.
852
+ //
853
+ // Renders ONLY the JS guide / tooltip / spotlight runtime (DigiaGuideRuntime).
854
+ // Native overlays (nudges, dialogs, bottom sheets, surveys) render through the
855
+ // single native overlay host that DigiaModule mounts imperatively after
856
+ // Digia.initialize() — that host owns its own touch handling and claims touches
857
+ // only while an overlay is active. Mounting a native host here as well would
858
+ // render every overlay twice (one stacked behind the other), and a
859
+ // React-tree host is not reliably touch-correct under the New Architecture.
860
+
861
+ export function DigiaHost({ children }: { children?: React.ReactNode }) {
862
+ if (children != null) {
863
+ return (
864
+ <>
865
+ {children}
866
+ <DigiaGuideRuntime />
867
+ </>
868
+ );
869
+ }
814
870
 
815
- export function DigiaHost() {
816
871
  return <DigiaGuideRuntime />;
817
872
  }
818
873
 
@@ -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