@digia-engage/core 2.4.0 → 2.5.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digia-engage/core",
3
- "version": "2.4.0",
3
+ "version": "2.5.1",
4
4
  "description": "React Native bridge for Digia Engage – renders native Android Compose UI inside React Native apps",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
package/src/Digia.ts CHANGED
@@ -205,7 +205,7 @@ class DigiaClass implements DigiaDelegate {
205
205
  // Mirrors DigiaCEPDelegate on Android.
206
206
  // Forwards to the native DigiaCEPDelegate via the bridge.
207
207
 
208
- async onCampaignTriggered(payload: CEPTriggerPayload): Promise<void> {
208
+ async onCampaignTriggered(payload: CEPTriggerPayload): Promise<boolean> {
209
209
  if (!this._nativeBridgeWired) {
210
210
  digiaHealthReporter.report(HealthEventType.plugin_not_registered, { campaign_key: payload.campaignKey });
211
211
  }
@@ -222,7 +222,7 @@ class DigiaClass implements DigiaDelegate {
222
222
  const result = evaluate(policy, state, Date.now());
223
223
  if (!result.allow) {
224
224
  this._log(`frequency_capped campaign_key=${campaignKey} reason=${result.reason}`);
225
- return;
225
+ return false;
226
226
  }
227
227
  }
228
228
 
@@ -237,7 +237,7 @@ class DigiaClass implements DigiaDelegate {
237
237
  this._emitSlotWidth(campaign);
238
238
  }
239
239
  nativeDigiaModule.triggerCampaign(cepCampaignId, bridgeContent, cepMetadata);
240
- return;
240
+ return true;
241
241
  }
242
242
 
243
243
  if (campaign?.campaign_type === 'guide') {
@@ -251,7 +251,7 @@ class DigiaClass implements DigiaDelegate {
251
251
  campaign_key: campaignKey,
252
252
  reason: 'guide_campaign_has_no_steps',
253
253
  });
254
- return;
254
+ return false;
255
255
  }
256
256
 
257
257
  const firstAnchorKey = config.steps[0].anchorKey;
@@ -263,7 +263,7 @@ class DigiaClass implements DigiaDelegate {
263
263
  reason: 'anchor_key_not_registered',
264
264
  anchor_key: firstAnchorKey,
265
265
  });
266
- return;
266
+ return false;
267
267
  }
268
268
 
269
269
  this._activePayloads.set(cepCampaignId, payload);
@@ -275,6 +275,10 @@ class DigiaClass implements DigiaDelegate {
275
275
  variables,
276
276
  config,
277
277
  onExperienceEvent: (event) => this._onGuideLifecycleEvent(event, cepCampaignId, campaignKey, digiaId),
278
+ // Safety net: release the CEP slot on every guide exit, including
279
+ // CTA-close / advance-past-end / anchor-drop paths that emit no
280
+ // terminal lifecycle event. Idempotent with the explicit path below.
281
+ onEnd: () => this._releaseGuideSlot(cepCampaignId),
278
282
  });
279
283
 
280
284
  this._log(`guide trigger campaign_key=${campaignKey} mounted=${mounted}`);
@@ -284,8 +288,10 @@ class DigiaClass implements DigiaDelegate {
284
288
  campaign_key: campaignKey,
285
289
  payload_id: cepCampaignId,
286
290
  });
291
+ this._activePayloads.delete(cepCampaignId);
292
+ return false;
287
293
  }
288
- return;
294
+ return true;
289
295
  }
290
296
 
291
297
  if (!campaign) {
@@ -295,11 +301,12 @@ class DigiaClass implements DigiaDelegate {
295
301
  payload_id: cepCampaignId,
296
302
  available_campaign_keys: [...this._campaignsByKey.keys()],
297
303
  });
298
- return;
304
+ return false;
299
305
  }
300
306
 
301
307
  this._activePayloads.set(cepCampaignId, payload);
302
308
  nativeDigiaModule.triggerCampaign(cepCampaignId, bridgeContent, cepMetadata);
309
+ return true;
303
310
  }
304
311
 
305
312
  onCampaignInvalidated(campaignId: string): void {
@@ -404,14 +411,18 @@ class DigiaClass implements DigiaDelegate {
404
411
 
405
412
  // Notify plugins of CEP lifecycle termination (template cleanup) on exit events.
406
413
  if (event.type === 'dismissed' || event.type === 'completed') {
407
- const storedPayload = this._activePayloads.get(payloadId);
408
- if (storedPayload) {
409
- this._plugins.forEach((p) => p.notifyEvent({ type: 'dismissed' }, storedPayload));
410
- this._activePayloads.delete(payloadId);
411
- }
414
+ this._releaseGuideSlot(payloadId);
412
415
  }
413
416
  }
414
417
 
418
+ /** Releases the CEP in-app slot for a guide payload. Idempotent. */
419
+ private _releaseGuideSlot(payloadId: string): void {
420
+ const storedPayload = this._activePayloads.get(payloadId);
421
+ if (!storedPayload) return;
422
+ this._plugins.forEach((p) => p.notifyEvent({ type: 'dismissed' }, storedPayload));
423
+ this._activePayloads.delete(payloadId);
424
+ }
425
+
415
426
  private _guideEventName(type: GuideLifecycleEvent['type']): string {
416
427
  switch (type) {
417
428
  case 'viewed': return 'Digia Experience Viewed';
@@ -5,11 +5,11 @@ import type { VariableMap } from './interpolate';
5
5
  export interface DigiaGuideRequest {
6
6
  payloadId: string;
7
7
  campaignKey: string;
8
- /** Digia backend UUID for this campaign (from the campaign store _id field). */
9
8
  campaignId: string;
10
9
  variables?: VariableMap;
11
10
  config: TemplateConfig;
12
11
  onExperienceEvent: (event: GuideLifecycleEvent) => void;
12
+ onEnd?: () => void;
13
13
  }
14
14
 
15
15
  type DigiaGuideControllerEvent =
@@ -46,8 +46,10 @@ class DigiaGuideController {
46
46
  cancel(payloadId: string): void {
47
47
  this._queue = this._queue.filter(r => r.payloadId !== payloadId);
48
48
  if (this._activeRequest?.payloadId === payloadId) {
49
+ const ended = this._activeRequest;
49
50
  this._listener?.({ type: 'cancel', payloadId });
50
51
  this._activeRequest = null;
52
+ ended.onEnd?.();
51
53
  this._dispatchNext();
52
54
  }
53
55
  }
@@ -8,16 +8,9 @@ import {
8
8
  StyleSheet,
9
9
  Text,
10
10
  View,
11
- requireNativeComponent,
12
11
  useWindowDimensions,
13
12
  } from 'react-native';
14
13
 
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;
21
14
  import { computePosition, flip, offset, shift } from '@floating-ui/core';
22
15
  import Svg, { Path } from 'react-native-svg';
23
16
  import { Digia } from './Digia';
@@ -857,37 +850,25 @@ function DigiaGuideRuntime() {
857
850
  // Place once at the app root. Accepts optional children (wrap mode) or can be
858
851
  // used standalone (<DigiaHost />) as a sibling alongside other root elements.
859
852
  //
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)
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.
863
860
 
864
861
  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
862
  if (children != null) {
882
863
  return (
883
864
  <>
884
865
  {children}
885
- {overlay}
866
+ <DigiaGuideRuntime />
886
867
  </>
887
868
  );
888
869
  }
889
870
 
890
- return overlay;
871
+ return <DigiaGuideRuntime />;
891
872
  }
892
873
 
893
874
  // ─── Styles ───────────────────────────────────────────────────────────────────
package/src/types.ts CHANGED
@@ -73,8 +73,13 @@ export type GuideLifecycleEvent =
73
73
  * Call these instead of touching Digia directly from inside a plugin.
74
74
  */
75
75
  export interface DigiaDelegate {
76
- /** Deliver a campaign payload into the Digia rendering engine. */
77
- onCampaignTriggered(payload: CEPTriggerPayload): void | Promise<void>;
76
+ /**
77
+ * Deliver a campaign payload into the Digia rendering engine.
78
+ * @returns `true` if accepted for display, `false` if dropped (frequency cap,
79
+ * missing anchor, unknown key, …). Plugins holding a display lock must
80
+ * release it on `false`.
81
+ */
82
+ onCampaignTriggered(payload: CEPTriggerPayload): boolean | Promise<boolean>;
78
83
  /** Invalidate / dismiss a campaign by its ID. */
79
84
  onCampaignInvalidated(campaignId: string): void;
80
85
  }