@digia-engage/core 1.1.1 → 2.0.0-rc.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.
Files changed (88) hide show
  1. package/README.md +134 -51
  2. package/android/build.gradle +2 -2
  3. package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +52 -8
  4. package/android/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +6 -2
  5. package/android/src/main/java/com/digia/engage/rn/DigiaViewManager.kt +1 -0
  6. package/ios/DigiaEngageModule.m +7 -1
  7. package/ios/DigiaHostViewManager.swift +20 -20
  8. package/ios/DigiaModule.swift +8 -4
  9. package/lib/commonjs/Digia.js +301 -3
  10. package/lib/commonjs/Digia.js.map +1 -1
  11. package/lib/commonjs/DigiaGuideController.js +59 -0
  12. package/lib/commonjs/DigiaGuideController.js.map +1 -0
  13. package/lib/commonjs/DigiaHealthReporter.js +45 -0
  14. package/lib/commonjs/DigiaHealthReporter.js.map +1 -0
  15. package/lib/commonjs/DigiaProvider.js +1079 -0
  16. package/lib/commonjs/DigiaProvider.js.map +1 -0
  17. package/lib/commonjs/DigiaSlotView.js +18 -3
  18. package/lib/commonjs/DigiaSlotView.js.map +1 -1
  19. package/lib/commonjs/NativeDigiaEngage.js +14 -8
  20. package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
  21. package/lib/commonjs/actionHandler.js +316 -0
  22. package/lib/commonjs/actionHandler.js.map +1 -0
  23. package/lib/commonjs/defaultInAppBrowser.js +31 -0
  24. package/lib/commonjs/defaultInAppBrowser.js.map +1 -0
  25. package/lib/commonjs/digiaAnchorRegistry.js +32 -0
  26. package/lib/commonjs/digiaAnchorRegistry.js.map +1 -0
  27. package/lib/commonjs/index.js +7 -0
  28. package/lib/commonjs/index.js.map +1 -1
  29. package/lib/commonjs/templateTypes.js +2 -0
  30. package/lib/commonjs/templateTypes.js.map +1 -0
  31. package/lib/module/Digia.js +301 -3
  32. package/lib/module/Digia.js.map +1 -1
  33. package/lib/module/DigiaGuideController.js +53 -0
  34. package/lib/module/DigiaGuideController.js.map +1 -0
  35. package/lib/module/DigiaHealthReporter.js +38 -0
  36. package/lib/module/DigiaHealthReporter.js.map +1 -0
  37. package/lib/module/DigiaProvider.js +1072 -0
  38. package/lib/module/DigiaProvider.js.map +1 -0
  39. package/lib/module/DigiaSlotView.js +20 -5
  40. package/lib/module/DigiaSlotView.js.map +1 -1
  41. package/lib/module/NativeDigiaEngage.js +14 -8
  42. package/lib/module/NativeDigiaEngage.js.map +1 -1
  43. package/lib/module/actionHandler.js +311 -0
  44. package/lib/module/actionHandler.js.map +1 -0
  45. package/lib/module/defaultInAppBrowser.js +25 -0
  46. package/lib/module/defaultInAppBrowser.js.map +1 -0
  47. package/lib/module/digiaAnchorRegistry.js +26 -0
  48. package/lib/module/digiaAnchorRegistry.js.map +1 -0
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/templateTypes.js +2 -0
  52. package/lib/module/templateTypes.js.map +1 -0
  53. package/lib/typescript/Digia.d.ts +29 -2
  54. package/lib/typescript/Digia.d.ts.map +1 -1
  55. package/lib/typescript/DigiaGuideController.d.ts +30 -0
  56. package/lib/typescript/DigiaGuideController.d.ts.map +1 -0
  57. package/lib/typescript/DigiaHealthReporter.d.ts +24 -0
  58. package/lib/typescript/DigiaHealthReporter.d.ts.map +1 -0
  59. package/lib/typescript/DigiaProvider.d.ts +3 -0
  60. package/lib/typescript/DigiaProvider.d.ts.map +1 -0
  61. package/lib/typescript/DigiaSlotView.d.ts.map +1 -1
  62. package/lib/typescript/NativeDigiaEngage.d.ts +10 -6
  63. package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
  64. package/lib/typescript/actionHandler.d.ts +20 -0
  65. package/lib/typescript/actionHandler.d.ts.map +1 -0
  66. package/lib/typescript/defaultInAppBrowser.d.ts +3 -0
  67. package/lib/typescript/defaultInAppBrowser.d.ts.map +1 -0
  68. package/lib/typescript/digiaAnchorRegistry.d.ts +15 -0
  69. package/lib/typescript/digiaAnchorRegistry.d.ts.map +1 -0
  70. package/lib/typescript/index.d.ts +1 -0
  71. package/lib/typescript/index.d.ts.map +1 -1
  72. package/lib/typescript/templateTypes.d.ts +140 -0
  73. package/lib/typescript/templateTypes.d.ts.map +1 -0
  74. package/lib/typescript/types.d.ts +140 -3
  75. package/lib/typescript/types.d.ts.map +1 -1
  76. package/package.json +11 -3
  77. package/src/Digia.ts +340 -3
  78. package/src/DigiaGuideController.ts +61 -0
  79. package/src/DigiaHealthReporter.ts +43 -0
  80. package/src/DigiaProvider.tsx +776 -0
  81. package/src/DigiaSlotView.tsx +26 -6
  82. package/src/NativeDigiaEngage.ts +28 -13
  83. package/src/actionHandler.ts +311 -0
  84. package/src/defaultInAppBrowser.ts +31 -0
  85. package/src/digiaAnchorRegistry.ts +27 -0
  86. package/src/index.ts +1 -0
  87. package/src/templateTypes.ts +121 -0
  88. package/src/types.ts +102 -5
@@ -6,8 +6,9 @@
6
6
  * pass an explicit `height` in `style` to fix the size instead.
7
7
  */
8
8
 
9
- import React, { useCallback, useState } from 'react';
9
+ import React, { useCallback, useEffect, useState } from 'react';
10
10
  import {
11
+ DeviceEventEmitter,
11
12
  Platform,
12
13
  StyleSheet,
13
14
  requireNativeComponent,
@@ -21,7 +22,7 @@ interface DigiaSlotViewProps {
21
22
  }
22
23
 
23
24
  interface NativeDigiaSlotViewProps extends DigiaSlotViewProps {
24
- onContentSizeChange?: (event: { nativeEvent: { height: number } }) => void;
25
+ onContentSizeChange?: (event: { nativeEvent: { height: number; width: number } }) => void;
25
26
  collapsable?: boolean;
26
27
  }
27
28
 
@@ -32,24 +33,43 @@ const NativeDigiaSlotView =
32
33
 
33
34
  export function DigiaSlotView({ placementKey, style }: DigiaSlotViewProps) {
34
35
  const [contentHeight, setContentHeight] = useState(0);
36
+ const [contentWidth, setContentWidth] = useState<number | null>(null);
37
+
38
+ useEffect(() => {
39
+ console.log('[DigiaSlotView:debug] mounted placementKey=' + placementKey);
40
+ const sub = DeviceEventEmitter.addListener(
41
+ 'digiaSlotWidth',
42
+ (data: { slotKey: string; width: number | null }) => {
43
+ console.log('[DigiaSlotView:debug] digiaSlotWidth event received', JSON.stringify(data), 'myKey=' + placementKey, 'match=' + (data.slotKey === placementKey));
44
+ if (data.slotKey === placementKey) {
45
+ setContentWidth(data.width && data.width > 0 ? data.width : null);
46
+ }
47
+ },
48
+ );
49
+ return () => sub.remove();
50
+ }, [placementKey]);
35
51
 
36
52
  const onContentSizeChange = useCallback(
37
- (event: { nativeEvent: { height: number } }) => {
53
+ (event: { nativeEvent: { height: number; width: number } }) => {
38
54
  const h = event.nativeEvent.height ?? 0;
55
+ const w = event.nativeEvent.width ?? 0;
56
+ console.log('[DigiaSlotView:debug] onContentSizeChange placementKey=' + placementKey + ' h=' + h + ' w=' + w);
39
57
  setContentHeight(Math.max(0, h));
58
+ setContentWidth(w > 0 ? w : null);
40
59
  },
41
- [],
60
+ [placementKey],
42
61
  );
43
62
 
44
63
  if ((Platform.OS === 'android' || Platform.OS === 'ios') && NativeDigiaSlotView) {
45
64
  const flatStyle = StyleSheet.flatten(style) || {};
46
65
  const hasExplicitHeight = flatStyle.height !== undefined;
66
+ const resolvedWidth = contentWidth ?? '100%';
47
67
 
48
68
  if (hasExplicitHeight) {
49
69
  return (
50
70
  <NativeDigiaSlotView
51
71
  placementKey={placementKey}
52
- style={[{ width: '100%' }, style]}
72
+ style={[{ width: resolvedWidth }, style]}
53
73
  {...(Platform.OS === 'android' ? { collapsable: false } : {})}
54
74
  />
55
75
  );
@@ -61,7 +81,7 @@ export function DigiaSlotView({ placementKey, style }: DigiaSlotViewProps) {
61
81
  return (
62
82
  <NativeDigiaSlotView
63
83
  placementKey={placementKey}
64
- style={[{ width: '100%', height: bootstrapHeight }, style]}
84
+ style={[{ width: resolvedWidth, height: bootstrapHeight }, style]}
65
85
  onContentSizeChange={onContentSizeChange}
66
86
  {...(Platform.OS === 'android' ? { collapsable: false } : {})}
67
87
  />
@@ -1,15 +1,13 @@
1
1
  /**
2
2
  * NativeDigiaModule
3
3
  *
4
- * Low-level TurboModule binding to the native Digia Engage module.
4
+ * Low-level native binding to the Digia Engage module.
5
5
  *
6
6
  * The module is resolved lazily on first use (not at import time) so that
7
7
  * module evaluation before native initialisation doesn't throw.
8
8
  * Resolution order:
9
- * 1. TurboModuleRegistry.get() New Architecture / JSI path
10
- * 2. NativeModules bridge interop layer (RN 0.73+ New Arch
11
- * with isTurboModule: false in ReactModuleInfo)
12
- * 3. null — non-Android environments; methods no-op
9
+ * 1. NativeModules iOS bridge module
10
+ * 2. null non-Android environments; methods no-op
13
11
  *
14
12
  * Prefer using the high-level `Digia` singleton from `index.ts`.
15
13
  */
@@ -26,7 +24,7 @@ import { NativeModules, TurboModuleRegistry } from 'react-native';
26
24
  */
27
25
  export interface Spec extends TurboModule {
28
26
  /** Initialise the SDK. Call once before anything else. */
29
- initialize(apiKey: string, environment: string, logLevel: string): Promise<void>;
27
+ initialize(projectId: string, environment: string, logLevel: string, baseUrl?: string, fontFamily?: string): Promise<void>;
30
28
 
31
29
  /**
32
30
  * Wire the internal RNEventBridgePlugin with the native SDK.
@@ -51,6 +49,15 @@ export interface Spec extends TurboModule {
51
49
  /** Invalidate / dismiss a campaign by its ID. */
52
50
  invalidateCampaign(campaignId: string): void;
53
51
 
52
+ /** Register an anchor element position for tooltip/spotlight targeting. */
53
+ registerAnchor(key: string, x: number, y: number, width: number, height: number): void;
54
+
55
+ /** Remove a previously registered anchor. */
56
+ unregisterAnchor(key: string): void;
57
+
58
+ /** Return all component keys registered with the native SDK. */
59
+ getRegisteredComponents(): Promise<string[]>;
60
+
54
61
  }
55
62
 
56
63
 
@@ -61,12 +68,17 @@ export interface Spec extends TurboModule {
61
68
  // If neither resolves, warn in DEV and use no-op stubs so non-Android
62
69
  // environments (web, Storybook) don't crash.
63
70
  let _resolved: Spec | null = null;
71
+ let _didResolve = false;
72
+
73
+ function resolveCodegenModule(): Spec | null {
74
+ return TurboModuleRegistry.get<Spec>('DigiaEngageModule') ?? null;
75
+ }
76
+
64
77
  function getModule(): Spec | null {
65
- if (_resolved !== null) return _resolved;
66
- _resolved =
67
- TurboModuleRegistry.get<Spec>('DigiaEngageModule') ??
68
- (NativeModules.DigiaEngageModule as Spec | undefined) ??
69
- null;
78
+ if (_didResolve) return _resolved;
79
+ _didResolve = true;
80
+
81
+ _resolved = (NativeModules.DigiaEngageModule as Spec | undefined) ?? resolveCodegenModule();
70
82
  if (__DEV__ && !_resolved) {
71
83
  console.warn(
72
84
  '[Digia] DigiaEngageModule not found.\n' +
@@ -77,12 +89,15 @@ function getModule(): Spec | null {
77
89
  }
78
90
 
79
91
  export const nativeDigiaModule: Spec = {
80
- initialize: (apiKey, environment, logLevel) =>
81
- getModule()?.initialize(apiKey, environment, logLevel) ?? Promise.resolve(),
92
+ initialize: (projectId, environment, logLevel, baseUrl, fontFamily) =>
93
+ getModule()?.initialize(projectId, environment, logLevel, baseUrl, fontFamily) ?? Promise.resolve(),
82
94
  registerBridge: () => getModule()?.registerBridge(),
83
95
  setCurrentScreen: (name) => getModule()?.setCurrentScreen(name),
84
96
  triggerCampaign: (id, content, cepContext) =>
85
97
  getModule()?.triggerCampaign(id, content, cepContext),
86
98
  invalidateCampaign: (campaignId) => getModule()?.invalidateCampaign(campaignId),
99
+ registerAnchor: (key, x, y, width, height) => getModule()?.registerAnchor(key, x, y, width, height),
100
+ unregisterAnchor: (key) => getModule()?.unregisterAnchor(key),
101
+ getRegisteredComponents: () => getModule()?.getRegisteredComponents() ?? Promise.resolve([]),
87
102
  getConstants: () => getModule()?.getConstants?.() ?? {},
88
103
  };
@@ -0,0 +1,311 @@
1
+ import { AppState, type AppStateStatus, Linking } from 'react-native';
2
+ import type { DigiaAction, ActionContext, OnAction, InAppBrowserAdapter } from './types';
3
+ import type { Action } from './templateTypes';
4
+ import { digiaHealthReporter, HealthEventType } from './DigiaHealthReporter';
5
+
6
+ // ─── Types ────────────────────────────────────────────────────────────────────
7
+
8
+ export type ActionCallbacks = {
9
+ onNext: () => void;
10
+ onBack: () => void;
11
+ onDismissSelf: () => void;
12
+ onDismissAll: () => void;
13
+ };
14
+
15
+ type ActionHandlerConfig = {
16
+ onAction?: OnAction;
17
+ routeViaSystemLinking: boolean;
18
+ inAppBrowser?: InAppBrowserAdapter;
19
+ };
20
+
21
+ // ─── Internal state ───────────────────────────────────────────────────────────
22
+
23
+ let _config: ActionHandlerConfig = { routeViaSystemLinking: true };
24
+ let _lastActionKey = '';
25
+ let _lastActionAt = 0;
26
+ let _inappBrowserWarned = false;
27
+ const _invalidContextWarned = new Set<string>();
28
+ const MAX_WARNED_KEYS = 50;
29
+
30
+ const DEBOUNCE_MS = 500;
31
+ const HANDLER_TIMEOUT_MS = 2000;
32
+
33
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
34
+
35
+ function isValidUrl(url: string): boolean {
36
+ if (!url) return false;
37
+ try {
38
+ new URL(url);
39
+ return true;
40
+ } catch {
41
+ return /^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//.test(url);
42
+ }
43
+ }
44
+
45
+ function isDuplicate(action: DigiaAction, campaignId: string): boolean {
46
+ const key = `${campaignId}:${action.type}`;
47
+ const now = Date.now();
48
+ if (key === _lastActionKey && now - _lastActionAt < DEBOUNCE_MS) return true;
49
+ _lastActionKey = key;
50
+ _lastActionAt = now;
51
+ return false;
52
+ }
53
+
54
+ function emitHealth(eventType: HealthEventType, detail: Record<string, unknown>): void {
55
+ digiaHealthReporter.report(eventType, detail);
56
+ }
57
+
58
+ // ─── Convert widget Action → public DigiaAction ───────────────────────────────
59
+
60
+ export const toDigiaAction = (action: Action): DigiaAction => {
61
+ switch (action.type) {
62
+ case 'dismiss': return { type: 'dismiss', scope: action.scope };
63
+ case 'next': return { type: 'next' };
64
+ case 'back': return { type: 'back' };
65
+ case 'prev': return { type: 'back' };
66
+ case 'deep_link': return { type: 'deep_link', url: action.url, fallback_url: action.fallback_url };
67
+ case 'open_url': return { type: 'open_url', url: action.url, presentation: action.presentation };
68
+ }
69
+ };
70
+
71
+ // ─── onAction invocation with timeout + error handling ───────────────────────
72
+
73
+ async function callOnAction(
74
+ onAction: OnAction,
75
+ action: DigiaAction,
76
+ context: ActionContext,
77
+ ): Promise<boolean> {
78
+ const timedOut = { value: false };
79
+ try {
80
+ const result = onAction(action, context);
81
+ if (result === true) return true;
82
+ if (result === false || result === undefined || result === null) return false;
83
+ const timeoutPromise = new Promise<boolean>((resolve) =>
84
+ setTimeout(() => { timedOut.value = true; resolve(false); }, HANDLER_TIMEOUT_MS),
85
+ );
86
+ const resolved = await Promise.race([
87
+ Promise.resolve(result as Promise<boolean>).catch(() => false),
88
+ timeoutPromise,
89
+ ]);
90
+ if (timedOut.value) {
91
+ emitHealth(HealthEventType.action_handler_timeout, {
92
+ campaign_id: context.campaign_id,
93
+ action_type: action.type,
94
+ });
95
+ }
96
+ return resolved === true;
97
+ } catch (e) {
98
+ const err = e instanceof Error ? e : new Error(String(e));
99
+ emitHealth(HealthEventType.action_handler_threw, {
100
+ campaign_id: context.campaign_id,
101
+ action_type: action.type,
102
+ error_message: err.message,
103
+ error_stack: err.stack,
104
+ });
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // ─── Default behavior per action type ────────────────────────────────────────
110
+
111
+ async function runDefault(
112
+ action: DigiaAction,
113
+ context: ActionContext,
114
+ callbacks: ActionCallbacks,
115
+ ): Promise<void> {
116
+ switch (action.type) {
117
+ case 'deep_link': {
118
+ if (!isValidUrl(action.url)) {
119
+ emitHealth(HealthEventType.invalid_action_url, {
120
+ url: action.url, action_type: 'deep_link', campaign_id: context.campaign_id,
121
+ });
122
+ return;
123
+ }
124
+ const canOpen = await Linking.canOpenURL(action.url).catch(() => false);
125
+ if (canOpen) {
126
+ Linking.openURL(action.url).catch(() => {});
127
+ callbacks.onDismissSelf();
128
+ return;
129
+ }
130
+ if (action.fallback_url) {
131
+ if (!isValidUrl(action.fallback_url)) {
132
+ emitHealth(HealthEventType.invalid_action_url, {
133
+ url: action.fallback_url, action_type: 'deep_link', campaign_id: context.campaign_id,
134
+ });
135
+ } else {
136
+ const canOpenFallback = await Linking.canOpenURL(action.fallback_url).catch(() => false);
137
+ if (canOpenFallback) {
138
+ Linking.openURL(action.fallback_url).catch(() => {});
139
+ callbacks.onDismissSelf();
140
+ return;
141
+ }
142
+ }
143
+ }
144
+ emitHealth(HealthEventType.deep_link_no_handler, {
145
+ url: action.url,
146
+ ...(action.fallback_url ? { fallback_url: action.fallback_url } : {}),
147
+ campaign_id: context.campaign_id,
148
+ });
149
+ callbacks.onDismissSelf();
150
+ return;
151
+ }
152
+
153
+ case 'open_url': {
154
+ if (!isValidUrl(action.url)) {
155
+ emitHealth(HealthEventType.invalid_action_url, {
156
+ url: action.url, action_type: 'open_url', campaign_id: context.campaign_id,
157
+ });
158
+ return;
159
+ }
160
+ const presentation = action.presentation ?? 'external';
161
+ if (presentation === 'in_app' && _config.inAppBrowser) {
162
+ _config.inAppBrowser.open(action.url).catch(() => {});
163
+ } else {
164
+ if (presentation === 'in_app' && !_inappBrowserWarned) {
165
+ _inappBrowserWarned = true;
166
+ emitHealth(HealthEventType.inapp_browser_unavailable, { campaign_id: context.campaign_id });
167
+ }
168
+ Linking.openURL(action.url).catch(() => {});
169
+ }
170
+ callbacks.onDismissSelf();
171
+ return;
172
+ }
173
+
174
+ case 'dismiss': {
175
+ if (action.scope === 'all') {
176
+ callbacks.onDismissAll();
177
+ } else {
178
+ callbacks.onDismissSelf();
179
+ }
180
+ return;
181
+ }
182
+
183
+ case 'next': {
184
+ const isLastStep =
185
+ context.step_total !== undefined &&
186
+ context.step_index !== undefined &&
187
+ context.step_index >= context.step_total - 1;
188
+ if (isLastStep) {
189
+ const warnKey = `next:${context.campaign_id}`;
190
+ if (!_invalidContextWarned.has(warnKey)) {
191
+ if (_invalidContextWarned.size >= MAX_WARNED_KEYS) _invalidContextWarned.clear();
192
+ _invalidContextWarned.add(warnKey);
193
+ emitHealth(HealthEventType.invalid_action_context, {
194
+ campaign_id: context.campaign_id, action_type: 'next',
195
+ });
196
+ }
197
+ callbacks.onDismissSelf();
198
+ return;
199
+ }
200
+ callbacks.onNext();
201
+ return;
202
+ }
203
+
204
+ case 'back': {
205
+ if (context.step_index === 0) {
206
+ const warnKey = `back:${context.campaign_id}`;
207
+ if (!_invalidContextWarned.has(warnKey)) {
208
+ if (_invalidContextWarned.size >= MAX_WARNED_KEYS) _invalidContextWarned.clear();
209
+ _invalidContextWarned.add(warnKey);
210
+ emitHealth(HealthEventType.invalid_action_context, {
211
+ campaign_id: context.campaign_id, action_type: 'back',
212
+ });
213
+ }
214
+ return;
215
+ }
216
+ callbacks.onBack();
217
+ return;
218
+ }
219
+
220
+ }
221
+ }
222
+
223
+ // ─── Public interface ─────────────────────────────────────────────────────────
224
+
225
+ const configure = (config: Partial<ActionHandlerConfig>): void => {
226
+ _config = { routeViaSystemLinking: true, ...config };
227
+ };
228
+
229
+ const execute = async (
230
+ widgetAction: Action,
231
+ context: ActionContext,
232
+ callbacks: ActionCallbacks,
233
+ ): Promise<void> => {
234
+ const action = toDigiaAction(widgetAction);
235
+
236
+ if (isDuplicate(action, context.campaign_id)) return;
237
+
238
+ // Guard: queue if app not in foreground (cold-start scenario)
239
+ if (AppState.currentState !== 'active') {
240
+ digiaActionQueue.enqueue(widgetAction, action.type, context, callbacks);
241
+ return;
242
+ }
243
+
244
+ // 1. Analytics click — fire-and-forget (handled by caller via onExperienceEvent)
245
+
246
+ // 2. Invoke onAction override
247
+ let handled = false;
248
+ if (_config.onAction) {
249
+ handled = await callOnAction(_config.onAction, action, context);
250
+ }
251
+
252
+ // 3. Run default if not handled
253
+ if (!handled) {
254
+ await runDefault(action, context, callbacks);
255
+ }
256
+ };
257
+
258
+ export const digiaActionHandler = { configure, execute };
259
+
260
+ // ─── Cold-start queue ─────────────────────────────────────────────────────────
261
+
262
+ type QueuedItem = {
263
+ widgetAction: Action;
264
+ actionType: string;
265
+ context: ActionContext;
266
+ callbacks: ActionCallbacks;
267
+ };
268
+
269
+ const MAX_QUEUE_SIZE = 5;
270
+
271
+ class ActionQueue {
272
+ private _items: QueuedItem[] = [];
273
+ private _subscription: { remove(): void } | null = null;
274
+
275
+ constructor() {
276
+ this._subscription = AppState.addEventListener('change', (state: AppStateStatus) => {
277
+ if (state === 'active') this._flush();
278
+ });
279
+ }
280
+
281
+ enqueue(widgetAction: Action, actionType: string, context: ActionContext, callbacks: ActionCallbacks): void {
282
+ if (this._items.length >= MAX_QUEUE_SIZE) {
283
+ const dropped = this._items.shift()!;
284
+ emitHealth(HealthEventType.cold_start_queue_overflow, {
285
+ dropped_action_type: dropped.actionType,
286
+ dropped_campaign_id: dropped.context.campaign_id,
287
+ });
288
+ }
289
+ this._items.push({ widgetAction, actionType, context, callbacks });
290
+ }
291
+
292
+ destroy(): void {
293
+ this._subscription?.remove();
294
+ this._subscription = null;
295
+ this._items = [];
296
+ }
297
+
298
+ private _flush(): void {
299
+ const items = this._items.splice(0);
300
+ const processAt = (index: number) => {
301
+ if (index >= items.length) return;
302
+ const item = items[index];
303
+ digiaActionHandler.execute(item.widgetAction, item.context, item.callbacks).finally(() => {
304
+ setTimeout(() => processAt(index + 1), 100);
305
+ });
306
+ };
307
+ processAt(0);
308
+ }
309
+ }
310
+
311
+ const digiaActionQueue = new ActionQueue();
@@ -0,0 +1,31 @@
1
+ import type { InAppBrowserAdapter } from './types';
2
+
3
+ // Lazy accessor — throws at first access if the package is not installed,
4
+ // giving the developer a clear error at init time rather than silently
5
+ // falling back to Linking.openURL.
6
+ type InAppBrowserModule = { open(url: string, options?: Record<string, unknown>): Promise<void> };
7
+ let _module: InAppBrowserModule | null = null;
8
+
9
+ const loadModule = (): InAppBrowserModule => {
10
+ if (_module) return _module;
11
+ try {
12
+ // Dynamic require at runtime — not available at build time.
13
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
14
+ const pkg = (globalThis as any).require('react-native-inappbrowser-reborn');
15
+ if (!pkg) throw new Error('not installed');
16
+ _module = (pkg.InAppBrowser ?? pkg.default?.InAppBrowser ?? pkg) as InAppBrowserModule;
17
+ return _module;
18
+ } catch {
19
+ throw new Error(
20
+ '[Digia] defaultInAppBrowser requires react-native-inappbrowser-reborn. ' +
21
+ 'Run: npm install react-native-inappbrowser-reborn',
22
+ );
23
+ }
24
+ };
25
+
26
+ export const defaultInAppBrowser: InAppBrowserAdapter = {
27
+ open: async (url: string) => {
28
+ const browser = loadModule();
29
+ await browser!.open(url);
30
+ },
31
+ };
@@ -0,0 +1,27 @@
1
+ type AnchorLayout = { pageX: number; pageY: number; width: number; height: number }
2
+ type Listener = (layout: AnchorLayout) => void
3
+
4
+ const _layouts = new Map<string, AnchorLayout>()
5
+ const _listeners = new Map<string, Listener>()
6
+
7
+ const setLayout = (key: string, layout: AnchorLayout) => {
8
+ _layouts.set(key, layout)
9
+ _listeners.get(key)?.(layout)
10
+ }
11
+
12
+ const getLayout = (key: string): AnchorLayout | undefined => _layouts.get(key)
13
+
14
+ const subscribe = (key: string, listener: Listener): () => void => {
15
+ _listeners.set(key, listener)
16
+ const existing = _layouts.get(key)
17
+ if (existing) listener(existing)
18
+ return () => { if (_listeners.get(key) === listener) _listeners.delete(key) }
19
+ }
20
+
21
+ const remove = (key: string) => {
22
+ _layouts.delete(key)
23
+ _listeners.delete(key)
24
+ }
25
+
26
+ export type { AnchorLayout }
27
+ export const digiaAnchorRegistry = { setLayout, getLayout, subscribe, remove }
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@
11
11
 
12
12
  export { Digia } from './Digia';
13
13
  export { DigiaHostView } from './DigiaHostView';
14
+ export { DigiaHost } from './DigiaProvider';
14
15
  export { DigiaSlotView } from './DigiaSlotView';
15
16
  export { DigiaAnchorView } from './DigiaAnchorView';
16
17
  export type { DigiaConfig, DigiaDelegate, DigiaExperienceEvent, DigiaPlugin, InAppPayload } from './types';
@@ -0,0 +1,121 @@
1
+ export type Action =
2
+ | { type: 'dismiss'; label: string; style: 'primary' | 'secondary' | 'ghost'; scope?: 'self' | 'all' }
3
+ | { type: 'next'; label: string; style: 'primary' | 'secondary' | 'ghost' }
4
+ | { type: 'back'; label: string; style: 'primary' | 'secondary' | 'ghost' }
5
+ | { type: 'prev'; label: string; style: 'primary' | 'secondary' | 'ghost' }
6
+ | { type: 'deep_link'; label: string; style: 'primary' | 'secondary' | 'ghost'; url: string; fallback_url?: string }
7
+ | { type: 'open_url'; label: string; style: 'primary' | 'secondary' | 'ghost'; url: string; presentation: 'external' | 'in_app' }
8
+
9
+
10
+ export type TooltipStep = {
11
+ anchorKey: string
12
+ title: string
13
+ body: string
14
+ placement: 'top' | 'bottom' | 'left' | 'right' | 'auto'
15
+ backgroundColor: string
16
+ borderColor: string
17
+ borderWidth: number
18
+ cornerRadius: number
19
+ shadow: boolean
20
+ maxWidth: number
21
+ padding: number
22
+ showArrow: boolean
23
+ arrowColor?: string
24
+ arrowBorderColor?: string
25
+ arrowSize?: number
26
+ titleColor: string
27
+ titleSize: number
28
+ titleWeight: '400' | '600' | '700'
29
+ bodyColor: string
30
+ bodySize: number
31
+ buttonPrimaryBackgroundColor: string
32
+ buttonPrimaryTextColor: string
33
+ buttonGhostTextColor: string
34
+ actions: Action[]
35
+ }
36
+
37
+ export type SpotlightStep = {
38
+ anchorKey: string
39
+ title: string
40
+ body: string
41
+ calloutPosition: 'above' | 'below' | 'left' | 'right' | 'auto'
42
+ calloutGap?: number
43
+ overlayColor: string
44
+ overlayOpacity: number
45
+ highlightShape: 'rect' | 'circle' | 'pill'
46
+ highlightCornerRadius: number
47
+ highlightPadding: number
48
+ highlightGlowColor: string
49
+ highlightGlowWidth: number
50
+ calloutBackgroundColor: string
51
+ calloutCornerRadius: number
52
+ calloutMaxWidth: number
53
+ calloutPadding: number
54
+ calloutShadow: boolean
55
+ calloutBorderColor: string
56
+ calloutBorderWidth: number
57
+ showArrow?: boolean
58
+ arrowColor?: string
59
+ arrowBorderColor?: string
60
+ arrowSize?: number
61
+ titleColor: string
62
+ titleSize: number
63
+ titleWeight: '400' | '600' | '700'
64
+ bodyColor: string
65
+ bodySize: number
66
+ buttonPrimaryBackgroundColor: string
67
+ buttonPrimaryTextColor: string
68
+ buttonGhostTextColor: string
69
+ actions: Action[]
70
+ }
71
+
72
+ export type TooltipConfig = {
73
+ templateType: 'tooltip'
74
+ templateId: string | null
75
+ steps: TooltipStep[]
76
+ outsideTapBehavior?: 'dismiss' | 'next' | 'nothing'
77
+ }
78
+
79
+ export type SpotlightConfig = {
80
+ templateType: 'spotlight'
81
+ templateId: string | null
82
+ steps: SpotlightStep[]
83
+ outsideTapBehavior?: 'dismiss' | 'next' | 'nothing'
84
+ }
85
+
86
+ export type CarouselItem = { imageUrl: string; deepLink?: string }
87
+ export type CarouselIndicatorConfig = {
88
+ showIndicator: boolean
89
+ dotHeight: number
90
+ dotWidth: number
91
+ spacing: number
92
+ dotColor: string
93
+ activeDotColor: string
94
+ indicatorEffectType: 'slide' | 'expanding' | 'worm' | 'scale' | 'jumping' | 'scrolling'
95
+ }
96
+ export type CarouselConfig = {
97
+ templateType: 'carousel'
98
+ slotKey: string
99
+ items: CarouselItem[]
100
+ height: number
101
+ width?: number
102
+ autoPlay: boolean
103
+ autoPlayInterval: number
104
+ animationDuration: number
105
+ infiniteScroll: boolean
106
+ viewportFraction: number
107
+ indicator: CarouselIndicatorConfig
108
+ }
109
+
110
+ export type SurveyTemplateConfig = {
111
+ templateType: 'survey'
112
+ templateId: string
113
+ surveyName: string
114
+ uiTemplateId: string | null
115
+ settings: Record<string, unknown>
116
+ blocks: Record<string, unknown>[]
117
+ nodes: Record<string, unknown>[]
118
+ rootNodeId: string
119
+ }
120
+
121
+ export type TemplateConfig = TooltipConfig | SpotlightConfig | CarouselConfig | SurveyTemplateConfig