@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.
- package/README.md +134 -51
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +52 -8
- package/android/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +6 -2
- package/android/src/main/java/com/digia/engage/rn/DigiaViewManager.kt +1 -0
- package/ios/DigiaEngageModule.m +7 -1
- package/ios/DigiaHostViewManager.swift +20 -20
- package/ios/DigiaModule.swift +8 -4
- package/lib/commonjs/Digia.js +301 -3
- package/lib/commonjs/Digia.js.map +1 -1
- package/lib/commonjs/DigiaGuideController.js +59 -0
- package/lib/commonjs/DigiaGuideController.js.map +1 -0
- package/lib/commonjs/DigiaHealthReporter.js +45 -0
- package/lib/commonjs/DigiaHealthReporter.js.map +1 -0
- package/lib/commonjs/DigiaProvider.js +1079 -0
- package/lib/commonjs/DigiaProvider.js.map +1 -0
- package/lib/commonjs/DigiaSlotView.js +18 -3
- package/lib/commonjs/DigiaSlotView.js.map +1 -1
- package/lib/commonjs/NativeDigiaEngage.js +14 -8
- package/lib/commonjs/NativeDigiaEngage.js.map +1 -1
- package/lib/commonjs/actionHandler.js +316 -0
- package/lib/commonjs/actionHandler.js.map +1 -0
- package/lib/commonjs/defaultInAppBrowser.js +31 -0
- package/lib/commonjs/defaultInAppBrowser.js.map +1 -0
- package/lib/commonjs/digiaAnchorRegistry.js +32 -0
- package/lib/commonjs/digiaAnchorRegistry.js.map +1 -0
- package/lib/commonjs/index.js +7 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/templateTypes.js +2 -0
- package/lib/commonjs/templateTypes.js.map +1 -0
- package/lib/module/Digia.js +301 -3
- package/lib/module/Digia.js.map +1 -1
- package/lib/module/DigiaGuideController.js +53 -0
- package/lib/module/DigiaGuideController.js.map +1 -0
- package/lib/module/DigiaHealthReporter.js +38 -0
- package/lib/module/DigiaHealthReporter.js.map +1 -0
- package/lib/module/DigiaProvider.js +1072 -0
- package/lib/module/DigiaProvider.js.map +1 -0
- package/lib/module/DigiaSlotView.js +20 -5
- package/lib/module/DigiaSlotView.js.map +1 -1
- package/lib/module/NativeDigiaEngage.js +14 -8
- package/lib/module/NativeDigiaEngage.js.map +1 -1
- package/lib/module/actionHandler.js +311 -0
- package/lib/module/actionHandler.js.map +1 -0
- package/lib/module/defaultInAppBrowser.js +25 -0
- package/lib/module/defaultInAppBrowser.js.map +1 -0
- package/lib/module/digiaAnchorRegistry.js +26 -0
- package/lib/module/digiaAnchorRegistry.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/templateTypes.js +2 -0
- package/lib/module/templateTypes.js.map +1 -0
- package/lib/typescript/Digia.d.ts +29 -2
- package/lib/typescript/Digia.d.ts.map +1 -1
- package/lib/typescript/DigiaGuideController.d.ts +30 -0
- package/lib/typescript/DigiaGuideController.d.ts.map +1 -0
- package/lib/typescript/DigiaHealthReporter.d.ts +24 -0
- package/lib/typescript/DigiaHealthReporter.d.ts.map +1 -0
- package/lib/typescript/DigiaProvider.d.ts +3 -0
- package/lib/typescript/DigiaProvider.d.ts.map +1 -0
- package/lib/typescript/DigiaSlotView.d.ts.map +1 -1
- package/lib/typescript/NativeDigiaEngage.d.ts +10 -6
- package/lib/typescript/NativeDigiaEngage.d.ts.map +1 -1
- package/lib/typescript/actionHandler.d.ts +20 -0
- package/lib/typescript/actionHandler.d.ts.map +1 -0
- package/lib/typescript/defaultInAppBrowser.d.ts +3 -0
- package/lib/typescript/defaultInAppBrowser.d.ts.map +1 -0
- package/lib/typescript/digiaAnchorRegistry.d.ts +15 -0
- package/lib/typescript/digiaAnchorRegistry.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/templateTypes.d.ts +140 -0
- package/lib/typescript/templateTypes.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +140 -3
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +11 -3
- package/src/Digia.ts +340 -3
- package/src/DigiaGuideController.ts +61 -0
- package/src/DigiaHealthReporter.ts +43 -0
- package/src/DigiaProvider.tsx +776 -0
- package/src/DigiaSlotView.tsx +26 -6
- package/src/NativeDigiaEngage.ts +28 -13
- package/src/actionHandler.ts +311 -0
- package/src/defaultInAppBrowser.ts +31 -0
- package/src/digiaAnchorRegistry.ts +27 -0
- package/src/index.ts +1 -0
- package/src/templateTypes.ts +121 -0
- package/src/types.ts +102 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@digia-engage/core",
|
|
3
|
-
"version": "
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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();
|