@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digia-engage/core",
3
- "version": "1.1.1",
3
+ "version": "2.0.0-rc.1",
4
4
  "description": "React Native bridge for Digia Engage – renders native Android Compose UI inside React Native apps",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -44,8 +44,14 @@
44
44
  },
45
45
  "author": "Digia Technology Private Limited",
46
46
  "license": "MIT",
47
- "peerDependencies": {},
47
+ "peerDependencies": {
48
+ "@floating-ui/core": "^1.0.0",
49
+ "react-native-reanimated": ">=3.0.0",
50
+ "react-native-svg": ">=13.0.0"
51
+ },
48
52
  "devDependencies": {
53
+ "@edwardloopez/react-native-coachmark": "^0.5.3",
54
+ "@floating-ui/core": "^1.7.5",
49
55
  "@react-native/eslint-config": "^0.73.0",
50
56
  "@types/react": "^18.2.0",
51
57
  "@types/react-native": "^0.73.0",
@@ -53,6 +59,8 @@
53
59
  "react": "18.2.0",
54
60
  "react-native": "0.73.0",
55
61
  "react-native-builder-bob": "^0.23.0",
62
+ "react-native-reanimated": "^3.19.3",
63
+ "react-native-svg": "^15.15.1",
56
64
  "typescript": "^5.2.0"
57
65
  },
58
66
  "react-native-builder-bob": {
@@ -77,4 +85,4 @@
77
85
  "javaPackageName": "com.digia.engage.rn"
78
86
  }
79
87
  }
80
- }
88
+ }
package/src/Digia.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * import { Digia } from '@digia/engage-react-native';
8
8
  *
9
9
  * // In your App entry point (e.g. App.tsx):
10
- * await Digia.initialize({ apiKey: 'YOUR_API_KEY' });
10
+ * await Digia.initialize({ projectId: 'YOUR_PROJECT_ID' });
11
11
  *
12
12
  * // Whenever your navigation screen changes:
13
13
  * Digia.setCurrentScreen('Home');
@@ -17,13 +17,31 @@
17
17
 
18
18
  import { DeviceEventEmitter } from 'react-native';
19
19
  import { nativeDigiaModule } from './NativeDigiaEngage';
20
+ import { digiaHealthReporter, HealthEventType } from './DigiaHealthReporter';
21
+ import { digiaGuideController } from './DigiaGuideController';
22
+ import { digiaActionHandler } from './actionHandler';
20
23
  import type {
24
+ CampaignType,
21
25
  DigiaConfig,
22
26
  DigiaDelegate,
23
27
  DigiaExperienceEvent,
24
28
  DigiaPlugin,
29
+ GuideLifecycleEvent,
25
30
  InAppPayload,
26
31
  } from './types';
32
+ import type { TemplateConfig } from './templateTypes';
33
+
34
+ const PRODUCTION_API_ROOT = 'https://app.digia.tech';
35
+ const SANDBOX_API_ROOT = 'https://dev.digia.tech';
36
+ const DIGIA_SDK_VERSION = '1.0.0';
37
+
38
+ interface SdkCampaign {
39
+ id?: string;
40
+ _id?: string;
41
+ campaign_key: string;
42
+ campaign_type: CampaignType;
43
+ templateConfig?: Record<string, unknown>;
44
+ }
27
45
 
28
46
  class DigiaClass implements DigiaDelegate {
29
47
  private readonly _plugins = new Map<string, DigiaPlugin>();
@@ -34,6 +52,13 @@ class DigiaClass implements DigiaDelegate {
34
52
  // the full InAppPayload when overlay lifecycle events arrive from native.
35
53
  private readonly _activePayloads = new Map<string, InAppPayload>();
36
54
  private _engageSubscription: { remove(): void } | null = null;
55
+ private _projectId = '';
56
+ private _apiBaseUrl = '';
57
+ private _logLevel: DigiaConfig['logLevel'] = 'error';
58
+ private _fontFamily: string | undefined;
59
+ private _currentScreen: string | null = null;
60
+ private readonly _campaignsByKey = new Map<string, SdkCampaign>();
61
+ private readonly _registeredAnchorKeys = new Set<string>();
37
62
 
38
63
  /**
39
64
  * Initialise the Digia Engage SDK.
@@ -44,7 +69,27 @@ class DigiaClass implements DigiaDelegate {
44
69
  async initialize(config: DigiaConfig): Promise<void> {
45
70
  const environment = config.environment ?? 'production';
46
71
  const logLevel = config.logLevel ?? 'error';
47
- await nativeDigiaModule.initialize(config.apiKey, environment, logLevel);
72
+ this._projectId = config.projectId;
73
+ this._apiBaseUrl = this._resolveApiBaseUrl(config);
74
+ this._logLevel = logLevel;
75
+ this._fontFamily = config.fontFamily?.trim() || undefined;
76
+ digiaHealthReporter.init(config.projectId, this._apiBaseUrl);
77
+
78
+ digiaActionHandler.configure({
79
+ onAction: config.onAction,
80
+ routeViaSystemLinking: config.linking?.routeViaSystemLinking ?? true,
81
+ inAppBrowser: config.linking?.inAppBrowser,
82
+ });
83
+
84
+ try {
85
+ await nativeDigiaModule.initialize(config.projectId, environment, logLevel, config.baseUrl, config.fontFamily);
86
+ } catch (e) {
87
+ // TODO: TO BE PICKED LATER @aditya-digia — health event reporting being removed
88
+ // digiaHealthReporter.report(HealthEventType.fetch_failed, { error_code: 0, platform: 'react_native' });
89
+ throw e;
90
+ }
91
+
92
+ await this._refreshCampaignStore();
48
93
  }
49
94
 
50
95
  /**
@@ -58,7 +103,7 @@ class DigiaClass implements DigiaDelegate {
58
103
  * ```ts
59
104
  * import { DigiaMoEngagePlugin } from '@digia/moengage-plugin';
60
105
  *
61
- * await Digia.initialize({ apiKey: 'YOUR_KEY' });
106
+ * await Digia.initialize({ projectId: 'YOUR_PROJECT_ID' });
62
107
  * Digia.register(new DigiaMoEngagePlugin({ moEngage: MoEngage }));
63
108
  * ```
64
109
  */
@@ -74,6 +119,7 @@ class DigiaClass implements DigiaDelegate {
74
119
  this._nativeBridgeWired = true;
75
120
  }
76
121
  plugin.setup(this);
122
+ (plugin as any).reportHealth?.(digiaHealthReporter);
77
123
  this._plugins.set(plugin.identifier, plugin);
78
124
  }
79
125
 
@@ -95,22 +141,107 @@ class DigiaClass implements DigiaDelegate {
95
141
  * All registered plugins will have forwardScreen() called automatically.
96
142
  */
97
143
  setCurrentScreen(name: string): void {
144
+ this._currentScreen = name;
98
145
  nativeDigiaModule.setCurrentScreen(name);
99
146
  this._plugins.forEach((plugin) => plugin.forwardScreen(name));
100
147
  }
101
148
 
149
+ registerAnchor(anchorKey: string, _screenName?: string | null): void {
150
+ const cleanAnchorKey = anchorKey.trim();
151
+ if (!cleanAnchorKey) return;
152
+ this._registeredAnchorKeys.add(cleanAnchorKey);
153
+ }
154
+
155
+ unregisterAnchor(anchorKey: string): void {
156
+ this._registeredAnchorKeys.delete(anchorKey);
157
+ }
158
+
159
+ /**
160
+ * Global font family configured via {@link initialize}, or `undefined` when
161
+ * none was set. Used by the JS-rendered guide overlays (tooltip/spotlight)
162
+ * so their text matches native-rendered campaigns.
163
+ */
164
+ get fontFamily(): string | undefined {
165
+ return this._fontFamily;
166
+ }
167
+
102
168
 
103
169
  // ── DigiaDelegate ────────────────────────────────────────────────────────
104
170
  // Mirrors DigiaCEPDelegate on Android.
105
171
  // Forwards to the native DigiaCEPDelegate via the bridge.
106
172
 
107
173
  onCampaignTriggered(payload: InAppPayload): void {
174
+ if (!this._nativeBridgeWired) {
175
+ digiaHealthReporter.report(HealthEventType.plugin_not_registered, { campaign_key: payload.id });
176
+ }
177
+
178
+ const campaignKey = this._extractCampaignKey(payload);
179
+ this._log(`onCampaignTriggered payloadId=${payload.id} extractedKey=${campaignKey} knownKeys=[${[...this._campaignsByKey.keys()].join(', ')}]`);
180
+
181
+ if (campaignKey) {
182
+ const campaign = this._campaignsByKey.get(campaignKey);
183
+ if (campaign?.campaign_type === 'inline' || campaign?.campaign_type === 'survey') {
184
+ this._log(`${campaign.campaign_type} campaign triggered campaign_key=${campaignKey}, forwarding to native`);
185
+ this._activePayloads.set(payload.id, payload);
186
+ if (campaign.campaign_type === 'inline') {
187
+ this._emitSlotWidth(campaign);
188
+ }
189
+ nativeDigiaModule.triggerCampaign(payload.id, payload.content, payload.cepContext);
190
+ return;
191
+ }
192
+
193
+ if (campaign?.campaign_type === 'guide') {
194
+ const config = this._parseTemplateConfig(campaign);
195
+ if (
196
+ !config ||
197
+ (config.templateType !== 'tooltip' && config.templateType !== 'spotlight') ||
198
+ config.steps.length === 0
199
+ ) {
200
+ digiaHealthReporter.report(HealthEventType.anchor_not_on_screen, {
201
+ campaign_key: campaignKey,
202
+ reason: 'guide_campaign_has_no_steps',
203
+ });
204
+ return;
205
+ }
206
+
207
+ this._activePayloads.set(payload.id, payload);
208
+ const digiaId = campaign._id ?? campaign.id ?? campaignKey;
209
+ const mounted = digiaGuideController.start({
210
+ payloadId: payload.id,
211
+ campaignKey,
212
+ campaignId: digiaId,
213
+ config,
214
+ onExperienceEvent: (event) => this._onGuideLifecycleEvent(event, payload.id, campaignKey, digiaId),
215
+ });
216
+
217
+ this._log(`guide trigger campaign_key=${campaignKey} mounted=${mounted}`);
218
+ if (!mounted) {
219
+ digiaHealthReporter.report(HealthEventType.host_not_mounted, {
220
+ campaign_key: campaignKey,
221
+ payload_id: payload.id,
222
+ });
223
+ }
224
+ return;
225
+ }
226
+
227
+ if (!campaign) {
228
+ this._log(`campaign_key_mismatch: no campaign found for key="${campaignKey}"`);
229
+ digiaHealthReporter.report(HealthEventType.campaign_key_mismatch, {
230
+ campaign_key: campaignKey,
231
+ payload_id: payload.id,
232
+ available_campaign_keys: [...this._campaignsByKey.keys()],
233
+ });
234
+ return;
235
+ }
236
+ }
237
+
108
238
  this._activePayloads.set(payload.id, payload);
109
239
  nativeDigiaModule.triggerCampaign(payload.id, payload.content, payload.cepContext);
110
240
  }
111
241
 
112
242
  onCampaignInvalidated(campaignId: string): void {
113
243
  this._activePayloads.delete(campaignId);
244
+ digiaGuideController.cancel(campaignId);
114
245
  nativeDigiaModule.invalidateCampaign(campaignId);
115
246
  }
116
247
 
@@ -158,6 +289,212 @@ class DigiaClass implements DigiaDelegate {
158
289
  this._plugins.forEach((plugin) => plugin.notifyEvent(event, payload));
159
290
  }
160
291
 
292
+ private _onGuideLifecycleEvent(
293
+ event: GuideLifecycleEvent,
294
+ payloadId: string,
295
+ campaignKey: string,
296
+ campaignId: string,
297
+ ): void {
298
+ const eventName = this._guideEventName(event.type);
299
+ const properties = this._buildGuideProperties(event, campaignId, campaignKey);
300
+ this._plugins.forEach((p) => p.track?.(eventName, properties));
301
+
302
+ // Notify plugins of CEP lifecycle termination (template cleanup) on exit events.
303
+ if (event.type === 'dismissed' || event.type === 'completed') {
304
+ const storedPayload = this._activePayloads.get(payloadId);
305
+ if (storedPayload) {
306
+ this._plugins.forEach((p) => p.notifyEvent({ type: 'dismissed' }, storedPayload));
307
+ this._activePayloads.delete(payloadId);
308
+ }
309
+ }
310
+ }
311
+
312
+ private _guideEventName(type: GuideLifecycleEvent['type']): string {
313
+ switch (type) {
314
+ case 'viewed': return 'Digia Experience Viewed';
315
+ case 'step_viewed': return 'Digia Step Viewed';
316
+ case 'clicked': return 'Digia Experience Clicked';
317
+ case 'step_clicked': return 'Digia Step Clicked';
318
+ case 'dismissed': return 'Digia Experience Dismissed';
319
+ case 'step_dismissed': return 'Digia Step Dismissed';
320
+ case 'completed': return 'Digia Experience Completed';
321
+ }
322
+ }
323
+
324
+ private _buildGuideProperties(
325
+ event: GuideLifecycleEvent,
326
+ campaignId: string,
327
+ campaignKey: string,
328
+ ): Record<string, unknown> {
329
+ const base: Record<string, unknown> = {
330
+ campaign_id: campaignId,
331
+ campaign_key: campaignKey,
332
+ campaign_type: 'guide',
333
+ display_style: event.displayStyle,
334
+ step_index: event.stepIndex + 1,
335
+ step_total: event.stepTotal,
336
+ anchor_key: event.anchorKey,
337
+ slot_key: null,
338
+ element_id: null,
339
+ cta_label: null,
340
+ action_type: null,
341
+ action_url: null,
342
+ dismiss_reason: null,
343
+ abandoned_at_step: null,
344
+ digia_sdk_version: DIGIA_SDK_VERSION,
345
+ digia_platform: 'react_native',
346
+ };
347
+
348
+ if (event.type === 'clicked' || event.type === 'step_clicked') {
349
+ base.element_id = event.elementId ?? null;
350
+ base.cta_label = event.ctaLabel;
351
+ base.action_type = event.actionType;
352
+ base.action_url = event.actionUrl ?? null;
353
+ } else if (event.type === 'dismissed' || event.type === 'step_dismissed') {
354
+ base.dismiss_reason = event.dismissReason;
355
+ base.abandoned_at_step = event.stepIndex + 1;
356
+ }
357
+
358
+ return base;
359
+ }
360
+
361
+ private _resolveApiBaseUrl(config: DigiaConfig): string {
362
+ const root = (config.baseUrl ??
363
+ (config.environment === 'sandbox' ? SANDBOX_API_ROOT : PRODUCTION_API_ROOT)).trim();
364
+ const cleanRoot = root.replace(/\/+$/, '');
365
+ return cleanRoot.endsWith('/api/v1') ? cleanRoot : `${cleanRoot}/api/v1`;
366
+ }
367
+
368
+ private async _refreshCampaignStore(): Promise<void> {
369
+ try {
370
+ this._log(`fetching campaigns from ${this._apiBaseUrl}/engage/sdk/getCampaigns`);
371
+ const campaigns = await this._sdkPost<SdkCampaign[]>('getCampaigns');
372
+ this._campaignsByKey.clear();
373
+ if (campaigns.length > 0) {
374
+ this._log(`campaign[0] raw keys: ${JSON.stringify(Object.keys(campaigns[0] as object))}`);
375
+ }
376
+ campaigns.forEach((campaign) => {
377
+ const raw = campaign as unknown as Record<string, unknown>;
378
+ const key = (typeof raw.campaign_key === 'string' && raw.campaign_key)
379
+ || (typeof raw.campaignKey === 'string' && raw.campaignKey)
380
+ || null;
381
+ const type = (typeof raw.campaign_type === 'string' && raw.campaign_type)
382
+ || (typeof raw.campaignType === 'string' && raw.campaignType)
383
+ || '';
384
+ if (key) {
385
+ this._campaignsByKey.set(key, { ...campaign, campaign_key: key, campaign_type: type as SdkCampaign['campaign_type'] });
386
+ }
387
+ });
388
+ this._log(`loaded ${campaigns.length} campaign(s): [${[...this._campaignsByKey.keys()].join(', ')}]`);
389
+ } catch (e) {
390
+ const reason = e instanceof Error ? e.message : String(e);
391
+ this._log(`getCampaigns FAILED: ${reason}`);
392
+ digiaHealthReporter.report(HealthEventType.fetch_failed, {
393
+ error_code: 0,
394
+ platform: 'react_native',
395
+ reason,
396
+ });
397
+ }
398
+ }
399
+
400
+ private async _sdkPost<T>(path: string, body: Record<string, unknown> = {}): Promise<T> {
401
+ const res = await fetch(`${this._apiBaseUrl}/engage/sdk/${path}`, {
402
+ method: 'POST',
403
+ headers: {
404
+ 'Content-Type': 'application/json',
405
+ 'x-digia-project-id': this._projectId,
406
+ },
407
+ body: JSON.stringify(body),
408
+ });
409
+
410
+ if (!res.ok) {
411
+ throw new Error(`${path} failed: HTTP ${res.status}`);
412
+ }
413
+
414
+ const json = await res.json();
415
+ return this._extractApiResponse<T>(json);
416
+ }
417
+
418
+ private _extractApiResponse<T>(json: unknown): T {
419
+ if (Array.isArray(json)) return json as T;
420
+ if (json && typeof json === 'object') {
421
+ const obj = json as Record<string, unknown>;
422
+ const data = obj.data;
423
+ if (data && typeof data === 'object' && 'response' in data) {
424
+ const value = (data as Record<string, unknown>).response;
425
+ if (value == null) throw new Error('SDK response.data.response is null');
426
+ return value as T;
427
+ }
428
+ if ('response' in obj) {
429
+ const value = obj.response;
430
+ if (value == null) throw new Error('SDK response.response is null');
431
+ return value as T;
432
+ }
433
+ }
434
+ throw new Error('SDK response missing data.response');
435
+ }
436
+
437
+ private _emitSlotWidth(campaign: SdkCampaign): void {
438
+ const raw = campaign as unknown as Record<string, unknown>;
439
+ const config = raw.templateConfig as Record<string, unknown> | undefined;
440
+ if (!config) {
441
+ this._log(`_emitSlotWidth: no templateConfig for campaign ${campaign.campaign_key}`);
442
+ return;
443
+ }
444
+ const slotKey = typeof config.slotKey === 'string' ? config.slotKey : null;
445
+ const width = typeof config.width === 'number' && config.width > 0 ? config.width : null;
446
+ this._log(`_emitSlotWidth slotKey=${slotKey} width=${width} campaign=${campaign.campaign_key}`);
447
+ if (slotKey) {
448
+ DeviceEventEmitter.emit('digiaSlotWidth', { slotKey, width });
449
+ } else {
450
+ this._log(`_emitSlotWidth: no slotKey in templateConfig ${JSON.stringify(config)}`);
451
+ }
452
+ }
453
+
454
+ private _extractCampaignKey(payload: InAppPayload): string | null {
455
+ const fromContent = this._extractString(payload.content, 'digiaKey', 'campaign_key', 'campaignKey');
456
+ if (fromContent) return fromContent;
457
+
458
+ const args = payload.content.args;
459
+ if (args && typeof args === 'object' && !Array.isArray(args)) {
460
+ const fromArgs = this._extractString(args as Record<string, unknown>, 'digiaKey', 'campaign_key', 'campaignKey');
461
+ if (fromArgs) return fromArgs;
462
+ }
463
+
464
+ if (this._campaignsByKey.has(payload.id)) return payload.id;
465
+ return null;
466
+ }
467
+
468
+ private _extractString(data: Record<string, unknown>, ...keys: string[]): string | null {
469
+ for (const key of keys) {
470
+ const value = data[key];
471
+ if (typeof value === 'string' && value.trim()) return value.trim();
472
+ }
473
+ return null;
474
+ }
475
+
476
+ private _parseTemplateConfig(campaign: SdkCampaign): TemplateConfig | null {
477
+ const c = campaign as unknown as Record<string, unknown>;
478
+ const raw = c.templateConfig as Record<string, unknown> | undefined;
479
+ if (!raw || typeof raw !== 'object') return null;
480
+ const type = raw.templateType;
481
+ if (type !== 'tooltip' && type !== 'spotlight' && type !== 'carousel') {
482
+ this._log(`unknown templateType="${type}" for campaign_key=${campaign.campaign_key}`);
483
+ return null;
484
+ }
485
+ if (type === 'carousel') {
486
+ return raw as TemplateConfig;
487
+ }
488
+ const steps = Array.isArray(raw.steps) ? raw.steps : [];
489
+ return { ...raw, templateType: type, steps } as TemplateConfig;
490
+ }
491
+
492
+ private _log(message: string): void {
493
+ if (this._logLevel !== 'verbose') return;
494
+ // eslint-disable-next-line no-console
495
+ console.log(`[Digia] ${message}`);
496
+ }
497
+
161
498
  }
162
499
 
163
500
  export const Digia = new DigiaClass();
@@ -0,0 +1,61 @@
1
+ import type { GuideLifecycleEvent } from './types';
2
+ import type { TemplateConfig } from './templateTypes';
3
+
4
+ export interface DigiaGuideRequest {
5
+ payloadId: string;
6
+ campaignKey: string;
7
+ /** Digia backend UUID for this campaign (from the campaign store _id field). */
8
+ campaignId: string;
9
+ config: TemplateConfig;
10
+ onExperienceEvent: (event: GuideLifecycleEvent) => void;
11
+ }
12
+
13
+ type DigiaGuideControllerEvent =
14
+ | { type: 'start'; request: DigiaGuideRequest }
15
+ | { type: 'cancel'; payloadId: string };
16
+
17
+ type DigiaGuideListener = (event: DigiaGuideControllerEvent) => void;
18
+
19
+ class DigiaGuideController {
20
+ private _listener: DigiaGuideListener | null = null;
21
+ private _queue: DigiaGuideRequest[] = [];
22
+ private _activeRequest: DigiaGuideRequest | null = null;
23
+
24
+ subscribe(listener: DigiaGuideListener): () => void {
25
+ this._listener = listener;
26
+ if (this._queue.length > 0) {
27
+ const request = this._queue.shift()!;
28
+ this._activeRequest = request;
29
+ listener({ type: 'start', request });
30
+ }
31
+ return () => { if (this._listener === listener) this._listener = null; };
32
+ }
33
+
34
+ start(request: DigiaGuideRequest): boolean {
35
+ if (!this._listener) {
36
+ this._queue.push(request);
37
+ return false;
38
+ }
39
+ this._activeRequest = request;
40
+ this._listener({ type: 'start', request });
41
+ return true;
42
+ }
43
+
44
+ cancel(payloadId: string): void {
45
+ this._queue = this._queue.filter(r => r.payloadId !== payloadId);
46
+ if (this._activeRequest?.payloadId === payloadId) {
47
+ this._listener?.({ type: 'cancel', payloadId });
48
+ this._activeRequest = null;
49
+ this._dispatchNext();
50
+ }
51
+ }
52
+
53
+ private _dispatchNext(): void {
54
+ if (this._queue.length === 0 || !this._listener) return;
55
+ const request = this._queue.shift()!;
56
+ this._activeRequest = request;
57
+ this._listener({ type: 'start', request });
58
+ }
59
+ }
60
+
61
+ export const digiaGuideController = new DigiaGuideController();
@@ -0,0 +1,43 @@
1
+ export const HealthEventType = {
2
+ campaign_key_mismatch: 'campaign_key_mismatch',
3
+ component_orphaned: 'component_orphaned',
4
+ anchor_not_on_screen: 'anchor_not_on_screen',
5
+ host_not_mounted: 'host_not_mounted',
6
+ plugin_not_registered: 'plugin_not_registered',
7
+ fetch_failed: 'fetch_failed',
8
+ action_handler_threw: 'action_handler_threw',
9
+ action_handler_timeout: 'action_handler_timeout',
10
+ deep_link_no_handler: 'deep_link_no_handler',
11
+ invalid_action_url: 'invalid_action_url',
12
+ inapp_browser_unavailable: 'inapp_browser_unavailable',
13
+ invalid_action_context: 'invalid_action_context',
14
+ cold_start_queue_overflow: 'cold_start_queue_overflow',
15
+ } as const;
16
+
17
+ export type HealthEventType = (typeof HealthEventType)[keyof typeof HealthEventType];
18
+
19
+ export class DigiaHealthReporter {
20
+ private _projectId = '';
21
+ private _baseUrl = '';
22
+
23
+ init(projectId: string, baseUrl: string): void {
24
+ this._projectId = projectId;
25
+ this._baseUrl = baseUrl;
26
+ }
27
+
28
+ report(eventType: HealthEventType, detail: Record<string, unknown>): void {
29
+ if (!this._projectId) return;
30
+ // TODO: TO BE PICKED LATER @aditya-digia — health event backend endpoint being removed
31
+ // fetch(`${this._baseUrl}/engage/sdk/recordHealthEvent`, {
32
+ // method: 'POST',
33
+ // headers: {
34
+ // 'Content-Type': 'application/json',
35
+ // 'x-digia-project-id': this._projectId,
36
+ // },
37
+ // body: JSON.stringify({ event_type: eventType, detail }),
38
+ // }).catch(() => { /* swallow */ });
39
+ if (__DEV__) console.log('[DigiaHealth]', eventType, detail);
40
+ }
41
+ }
42
+
43
+ export const digiaHealthReporter = new DigiaHealthReporter();