@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
@@ -0,0 +1,1072 @@
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { Animated, Dimensions, Modal, Pressable, StyleSheet, Text, View, useWindowDimensions } from 'react-native';
3
+ import { computePosition, flip, offset, shift } from '@floating-ui/core';
4
+ import Svg, { Path } from 'react-native-svg';
5
+ import { Digia } from './Digia';
6
+ import { digiaGuideController } from './DigiaGuideController';
7
+ import { digiaAnchorRegistry } from './digiaAnchorRegistry';
8
+ import { digiaActionHandler } from './actionHandler';
9
+ // ─── @floating-ui/core platform adapter ──────────────────────────────────────
10
+
11
+ const rnCorePlatform = {
12
+ getElementRects: ({
13
+ reference,
14
+ floating
15
+ }) => {
16
+ const r = typeof reference.getBoundingClientRect === 'function' ? reference.getBoundingClientRect() : {
17
+ x: 0,
18
+ y: 0,
19
+ width: 0,
20
+ height: 0
21
+ };
22
+ return {
23
+ reference: {
24
+ x: r.x ?? r.left ?? 0,
25
+ y: r.y ?? r.top ?? 0,
26
+ width: r.width,
27
+ height: r.height
28
+ },
29
+ floating: {
30
+ x: 0,
31
+ y: 0,
32
+ width: floating.w ?? 0,
33
+ height: floating.h ?? 0
34
+ }
35
+ };
36
+ },
37
+ getDimensions: element => ({
38
+ width: element.w ?? element.width ?? 0,
39
+ height: element.h ?? element.height ?? 0
40
+ }),
41
+ getClippingRect: () => {
42
+ const {
43
+ width,
44
+ height
45
+ } = Dimensions.get('window');
46
+ return {
47
+ x: 0,
48
+ y: 0,
49
+ width,
50
+ height,
51
+ top: 0,
52
+ left: 0,
53
+ bottom: height,
54
+ right: width
55
+ };
56
+ },
57
+ isElement: () => false
58
+ };
59
+ function makeVirtualRef(layout, padding = 0) {
60
+ return {
61
+ getBoundingClientRect: () => ({
62
+ x: layout.pageX - padding,
63
+ y: layout.pageY - padding,
64
+ width: layout.width + padding * 2,
65
+ height: layout.height + padding * 2,
66
+ top: layout.pageY - padding,
67
+ left: layout.pageX - padding,
68
+ bottom: layout.pageY + layout.height + padding,
69
+ right: layout.pageX + layout.width + padding
70
+ })
71
+ };
72
+ }
73
+ // ─── Arrow component ──────────────────────────────────────────────────────────
74
+ //
75
+ // arrowOffset: pixel distance from the start of the side (left for top/bottom,
76
+ // top for left/right) to the arrow tip center. When provided the arrow points
77
+ // at the anchor; when omitted it falls back to centering.
78
+
79
+ function GuideArrow({
80
+ placement,
81
+ color,
82
+ borderColor,
83
+ size,
84
+ arrowOffset
85
+ }) {
86
+ const s1 = size + 1;
87
+
88
+ // Horizontal: used for top / bottom placements (left offset within bubble width)
89
+ const hWrap = edge => arrowOffset !== undefined ? {
90
+ [edge]: -s1,
91
+ left: arrowOffset - s1,
92
+ width: s1 * 2
93
+ } : {
94
+ [edge]: -s1,
95
+ left: 0,
96
+ right: 0
97
+ };
98
+
99
+ // Vertical: used for left / right placements (top offset within bubble height)
100
+ const vWrap = edge => arrowOffset !== undefined ? {
101
+ [edge]: -s1,
102
+ top: arrowOffset - s1,
103
+ height: s1 * 2
104
+ } : {
105
+ [edge]: -s1,
106
+ top: 0,
107
+ bottom: 0
108
+ };
109
+ if (placement === 'bottom' || placement === 'below') {
110
+ // bubble below anchor → arrow at TOP pointing ▲ up
111
+ return /*#__PURE__*/React.createElement(View, {
112
+ style: [arrowS.wrap, hWrap('top')]
113
+ }, /*#__PURE__*/React.createElement(View, {
114
+ style: {
115
+ position: 'relative',
116
+ width: s1 * 2,
117
+ height: s1,
118
+ alignItems: 'center'
119
+ }
120
+ }, /*#__PURE__*/React.createElement(View, {
121
+ style: {
122
+ position: 'absolute',
123
+ top: 0,
124
+ width: 0,
125
+ height: 0,
126
+ borderStyle: 'solid',
127
+ borderLeftWidth: s1,
128
+ borderRightWidth: s1,
129
+ borderBottomWidth: s1,
130
+ borderTopWidth: 0,
131
+ borderLeftColor: 'transparent',
132
+ borderRightColor: 'transparent',
133
+ borderBottomColor: borderColor
134
+ }
135
+ }), /*#__PURE__*/React.createElement(View, {
136
+ style: {
137
+ position: 'absolute',
138
+ top: 1,
139
+ width: 0,
140
+ height: 0,
141
+ borderStyle: 'solid',
142
+ borderLeftWidth: size,
143
+ borderRightWidth: size,
144
+ borderBottomWidth: size,
145
+ borderTopWidth: 0,
146
+ borderLeftColor: 'transparent',
147
+ borderRightColor: 'transparent',
148
+ borderBottomColor: color
149
+ }
150
+ })));
151
+ }
152
+ if (placement === 'top' || placement === 'above') {
153
+ // bubble above anchor → arrow at BOTTOM pointing ▼ down
154
+ return /*#__PURE__*/React.createElement(View, {
155
+ style: [arrowS.wrap, hWrap('bottom')]
156
+ }, /*#__PURE__*/React.createElement(View, {
157
+ style: {
158
+ position: 'relative',
159
+ width: s1 * 2,
160
+ height: s1,
161
+ alignItems: 'center'
162
+ }
163
+ }, /*#__PURE__*/React.createElement(View, {
164
+ style: {
165
+ position: 'absolute',
166
+ top: 0,
167
+ width: 0,
168
+ height: 0,
169
+ borderStyle: 'solid',
170
+ borderLeftWidth: s1,
171
+ borderRightWidth: s1,
172
+ borderTopWidth: s1,
173
+ borderBottomWidth: 0,
174
+ borderLeftColor: 'transparent',
175
+ borderRightColor: 'transparent',
176
+ borderTopColor: borderColor
177
+ }
178
+ }), /*#__PURE__*/React.createElement(View, {
179
+ style: {
180
+ position: 'absolute',
181
+ top: 0,
182
+ width: 0,
183
+ height: 0,
184
+ borderStyle: 'solid',
185
+ borderLeftWidth: size,
186
+ borderRightWidth: size,
187
+ borderTopWidth: size,
188
+ borderBottomWidth: 0,
189
+ borderLeftColor: 'transparent',
190
+ borderRightColor: 'transparent',
191
+ borderTopColor: color
192
+ }
193
+ })));
194
+ }
195
+ if (placement === 'right') {
196
+ // bubble right of anchor → arrow at LEFT pointing ◀ left
197
+ return /*#__PURE__*/React.createElement(View, {
198
+ style: [arrowS.wrap, vWrap('left')]
199
+ }, /*#__PURE__*/React.createElement(View, {
200
+ style: {
201
+ position: 'relative',
202
+ width: s1,
203
+ height: s1 * 2,
204
+ justifyContent: 'center'
205
+ }
206
+ }, /*#__PURE__*/React.createElement(View, {
207
+ style: {
208
+ position: 'absolute',
209
+ left: 0,
210
+ width: 0,
211
+ height: 0,
212
+ borderStyle: 'solid',
213
+ borderTopWidth: s1,
214
+ borderBottomWidth: s1,
215
+ borderRightWidth: s1,
216
+ borderLeftWidth: 0,
217
+ borderTopColor: 'transparent',
218
+ borderBottomColor: 'transparent',
219
+ borderRightColor: borderColor
220
+ }
221
+ }), /*#__PURE__*/React.createElement(View, {
222
+ style: {
223
+ position: 'absolute',
224
+ left: 1,
225
+ width: 0,
226
+ height: 0,
227
+ borderStyle: 'solid',
228
+ borderTopWidth: size,
229
+ borderBottomWidth: size,
230
+ borderRightWidth: size,
231
+ borderLeftWidth: 0,
232
+ borderTopColor: 'transparent',
233
+ borderBottomColor: 'transparent',
234
+ borderRightColor: color
235
+ }
236
+ })));
237
+ }
238
+ if (placement === 'left') {
239
+ // bubble left of anchor → arrow at RIGHT pointing ▶ right
240
+ return /*#__PURE__*/React.createElement(View, {
241
+ style: [arrowS.wrap, vWrap('right')]
242
+ }, /*#__PURE__*/React.createElement(View, {
243
+ style: {
244
+ position: 'relative',
245
+ width: s1,
246
+ height: s1 * 2,
247
+ justifyContent: 'center'
248
+ }
249
+ }, /*#__PURE__*/React.createElement(View, {
250
+ style: {
251
+ position: 'absolute',
252
+ right: 0,
253
+ width: 0,
254
+ height: 0,
255
+ borderStyle: 'solid',
256
+ borderTopWidth: s1,
257
+ borderBottomWidth: s1,
258
+ borderLeftWidth: s1,
259
+ borderRightWidth: 0,
260
+ borderTopColor: 'transparent',
261
+ borderBottomColor: 'transparent',
262
+ borderLeftColor: borderColor
263
+ }
264
+ }), /*#__PURE__*/React.createElement(View, {
265
+ style: {
266
+ position: 'absolute',
267
+ right: 1,
268
+ width: 0,
269
+ height: 0,
270
+ borderStyle: 'solid',
271
+ borderTopWidth: size,
272
+ borderBottomWidth: size,
273
+ borderLeftWidth: size,
274
+ borderRightWidth: 0,
275
+ borderTopColor: 'transparent',
276
+ borderBottomColor: 'transparent',
277
+ borderLeftColor: color
278
+ }
279
+ })));
280
+ }
281
+ return null;
282
+ }
283
+ const arrowS = StyleSheet.create({
284
+ wrap: {
285
+ position: 'absolute',
286
+ alignItems: 'center',
287
+ justifyContent: 'center'
288
+ }
289
+ });
290
+
291
+ // ─── Arrow offset helper ──────────────────────────────────────────────────────
292
+ // Returns the pixel distance from the start of the bubble side (left for
293
+ // top/bottom, top for left/right) to where the arrow tip should point,
294
+ // clamped so the arrow stays inside the bubble's rounded corners.
295
+
296
+ function calcArrowOffset(placement, layout, floatPos, bubbleW, bubbleH, cornerRadius, arrowSize) {
297
+ const minPad = cornerRadius + arrowSize + 2;
298
+ const isHoriz = placement === 'top' || placement === 'bottom' || placement === 'above' || placement === 'below';
299
+ if (isHoriz) {
300
+ const anchorCenterX = layout.pageX + layout.width / 2;
301
+ const raw = anchorCenterX - floatPos.x;
302
+ return Math.max(minPad, Math.min(bubbleW - minPad, raw));
303
+ }
304
+ const anchorCenterY = layout.pageY + layout.height / 2;
305
+ const raw = anchorCenterY - floatPos.y;
306
+ return Math.max(minPad, Math.min(bubbleH - minPad, raw));
307
+ }
308
+
309
+ // ─── Shared action helpers ────────────────────────────────────────────────────
310
+
311
+ function ActionButton({
312
+ action,
313
+ btnPrimaryBg,
314
+ btnPrimaryText,
315
+ btnGhostText,
316
+ onPress
317
+ }) {
318
+ const isPrimary = action.style === 'primary';
319
+ const fontFamily = Digia.fontFamily;
320
+ return /*#__PURE__*/React.createElement(Pressable, {
321
+ onPress: onPress,
322
+ style: [s.button, isPrimary && {
323
+ backgroundColor: btnPrimaryBg
324
+ }]
325
+ }, /*#__PURE__*/React.createElement(Text, {
326
+ style: {
327
+ color: isPrimary ? btnPrimaryText : btnGhostText,
328
+ fontSize: 13,
329
+ fontWeight: '600',
330
+ fontFamily
331
+ }
332
+ }, action.label));
333
+ }
334
+
335
+ // ─── Tooltip overlay ──────────────────────────────────────────────────────────
336
+ // Rendered WITHOUT a Modal so sticky tooltips do not block underlying scrolls.
337
+ // DigiaHost must be placed at the app root level (after NavigationContainer)
338
+ // for absoluteFill to cover the full screen.
339
+
340
+ function TooltipOverlay({
341
+ request,
342
+ config
343
+ }) {
344
+ const [stepIndex, setStepIndex] = useState(0);
345
+ const [layout, setLayout] = useState(null);
346
+ const [floatPos, setFloatPos] = useState(null);
347
+ const [resolvedPlacement, setResolvedPlacement] = useState('bottom');
348
+ const [floatingSize, setFloatingSize] = useState(null);
349
+ const step = config.steps[stepIndex];
350
+ const {
351
+ width: screenW
352
+ } = useWindowDimensions();
353
+ const opacityAnim = useRef(new Animated.Value(1)).current;
354
+ const pendingFadeIn = useRef(false);
355
+ const fontFamily = Digia.fontFamily;
356
+ const arrowSize = step.arrowSize ?? 8;
357
+ const showArrow = step.showArrow !== false;
358
+ const gap = showArrow ? arrowSize + 4 : 8;
359
+ useEffect(() => {
360
+ setLayout(null);
361
+ setFloatPos(null);
362
+ return digiaAnchorRegistry.subscribe(step.anchorKey, l => {
363
+ setLayout(l);
364
+ });
365
+ }, [step.anchorKey]);
366
+ useEffect(() => {
367
+ if (!layout || !floatingSize) return;
368
+ const tooltipW = Math.min(step.maxWidth, screenW - 32);
369
+ const fpPlacement = step.placement === 'auto' ? 'bottom' : step.placement;
370
+ computePosition(makeVirtualRef(layout), {
371
+ w: Math.min(tooltipW, floatingSize.w),
372
+ h: floatingSize.h
373
+ }, {
374
+ platform: rnCorePlatform,
375
+ placement: fpPlacement,
376
+ middleware: [offset(gap), flip(), shift({
377
+ padding: 16
378
+ })]
379
+ }).then(({
380
+ x,
381
+ y,
382
+ placement
383
+ }) => {
384
+ setFloatPos({
385
+ x,
386
+ y
387
+ });
388
+ setResolvedPlacement(placement);
389
+ });
390
+ }, [layout, floatingSize, step.placement, step.maxWidth, screenW, gap]);
391
+ useEffect(() => {
392
+ if (floatPos && pendingFadeIn.current) {
393
+ pendingFadeIn.current = false;
394
+ Animated.timing(opacityAnim, {
395
+ toValue: 1,
396
+ duration: 180,
397
+ useNativeDriver: true
398
+ }).start();
399
+ }
400
+ }, [floatPos, opacityAnim]);
401
+
402
+ // Fire viewed/step_viewed when the step renders.
403
+ useEffect(() => {
404
+ const isMultiStep = config.steps.length > 1;
405
+ if (stepIndex === 0) {
406
+ request.onExperienceEvent({
407
+ type: 'viewed',
408
+ stepIndex: 0,
409
+ stepTotal: config.steps.length,
410
+ anchorKey: step.anchorKey,
411
+ displayStyle: 'tooltip'
412
+ });
413
+ }
414
+ if (isMultiStep) {
415
+ request.onExperienceEvent({
416
+ type: 'step_viewed',
417
+ stepIndex,
418
+ stepTotal: config.steps.length,
419
+ anchorKey: step.anchorKey,
420
+ displayStyle: 'tooltip'
421
+ });
422
+ }
423
+ // eslint-disable-next-line react-hooks/exhaustive-deps
424
+ }, [stepIndex]);
425
+
426
+ // Closes guide without firing analytics — used after CTA actions have already fired clicked events.
427
+ const closeFromCTA = useCallback(() => {
428
+ Animated.timing(opacityAnim, {
429
+ toValue: 0,
430
+ duration: 150,
431
+ useNativeDriver: true
432
+ }).start(() => {
433
+ digiaGuideController.cancel(request.payloadId);
434
+ });
435
+ }, [opacityAnim, request]);
436
+
437
+ // Fires dismissed analytics then closes — used for non-CTA dismissals (scrim, back gesture).
438
+ const dismiss = useCallback((reason = 'scrim_tap') => {
439
+ Animated.timing(opacityAnim, {
440
+ toValue: 0,
441
+ duration: 150,
442
+ useNativeDriver: true
443
+ }).start(() => {
444
+ const isMultiStep = config.steps.length > 1;
445
+ request.onExperienceEvent({
446
+ type: 'dismissed',
447
+ stepIndex,
448
+ stepTotal: config.steps.length,
449
+ anchorKey: step.anchorKey,
450
+ displayStyle: 'tooltip',
451
+ dismissReason: reason
452
+ });
453
+ if (isMultiStep) {
454
+ request.onExperienceEvent({
455
+ type: 'step_dismissed',
456
+ stepIndex,
457
+ stepTotal: config.steps.length,
458
+ anchorKey: step.anchorKey,
459
+ displayStyle: 'tooltip',
460
+ dismissReason: reason
461
+ });
462
+ }
463
+ digiaGuideController.cancel(request.payloadId);
464
+ });
465
+ }, [request, opacityAnim, step, config, stepIndex]);
466
+ const stepTo = useCallback(newIndex => {
467
+ Animated.timing(opacityAnim, {
468
+ toValue: 0,
469
+ duration: 150,
470
+ useNativeDriver: true
471
+ }).start(() => {
472
+ if (newIndex === null) {
473
+ digiaGuideController.cancel(request.payloadId);
474
+ } else {
475
+ pendingFadeIn.current = true;
476
+ setStepIndex(newIndex);
477
+ }
478
+ });
479
+ }, [opacityAnim, request]);
480
+ const next = useCallback(() => stepTo(stepIndex < config.steps.length - 1 ? stepIndex + 1 : null), [stepIndex, config.steps.length, stepTo]);
481
+ const prev = useCallback(() => {
482
+ if (stepIndex > 0) stepTo(stepIndex - 1);
483
+ }, [stepIndex, stepTo]);
484
+ const actionCallbacks = useCallback(() => ({
485
+ onNext: next,
486
+ onBack: prev,
487
+ onDismissSelf: closeFromCTA,
488
+ onDismissAll: closeFromCTA
489
+ }), [next, prev, closeFromCTA]);
490
+ const tooltipW = Math.min(step.maxWidth, screenW - 32);
491
+ const arrowOffset = showArrow && floatPos && layout && floatingSize ? calcArrowOffset(resolvedPlacement, layout, floatPos, tooltipW, floatingSize.h, step.cornerRadius, arrowSize) : undefined;
492
+ const handleBackdropPress = useCallback(() => {
493
+ const behavior = config.outsideTapBehavior ?? 'next';
494
+ if (behavior === 'nothing') return;
495
+ if (behavior === 'next') next();
496
+ if (behavior === 'dismiss') dismiss();
497
+ }, [config.outsideTapBehavior, next, dismiss]);
498
+ return /*#__PURE__*/React.createElement(Modal, {
499
+ transparent: true,
500
+ statusBarTranslucent: true,
501
+ animationType: "none",
502
+ visible: true
503
+ }, /*#__PURE__*/React.createElement(Animated.View, {
504
+ style: [StyleSheet.absoluteFill, {
505
+ opacity: opacityAnim
506
+ }]
507
+ }, /*#__PURE__*/React.createElement(Pressable, {
508
+ style: StyleSheet.absoluteFill,
509
+ onPress: handleBackdropPress
510
+ }), floatPos ?
511
+ /*#__PURE__*/
512
+ // Bubble as Pressable so tapping the bubble body also advances
513
+ React.createElement(Pressable, {
514
+ onLayout: e => {
515
+ const {
516
+ width,
517
+ height
518
+ } = e.nativeEvent.layout;
519
+ if (floatingSize?.w !== width || floatingSize?.h !== height) {
520
+ setFloatingSize({
521
+ w: width,
522
+ h: height
523
+ });
524
+ }
525
+ },
526
+ onPress: handleBackdropPress,
527
+ style: [s.tooltipBubble, {
528
+ left: floatPos.x,
529
+ top: floatPos.y,
530
+ width: tooltipW,
531
+ backgroundColor: step.backgroundColor,
532
+ borderRadius: step.cornerRadius,
533
+ borderWidth: step.borderWidth,
534
+ borderColor: step.borderColor,
535
+ padding: step.padding
536
+ }, step.shadow && s.shadow]
537
+ }, showArrow && /*#__PURE__*/React.createElement(GuideArrow, {
538
+ placement: resolvedPlacement,
539
+ color: step.arrowColor ?? step.backgroundColor,
540
+ borderColor: step.arrowBorderColor ?? step.borderColor,
541
+ size: arrowSize,
542
+ arrowOffset: arrowOffset
543
+ }), /*#__PURE__*/React.createElement(Text, {
544
+ style: {
545
+ color: step.titleColor,
546
+ fontSize: step.titleSize,
547
+ fontWeight: step.titleWeight,
548
+ fontFamily
549
+ }
550
+ }, step.title), !!step.body && /*#__PURE__*/React.createElement(Text, {
551
+ style: {
552
+ marginTop: 4,
553
+ color: step.bodyColor,
554
+ fontSize: step.bodySize,
555
+ fontFamily
556
+ }
557
+ }, step.body), /*#__PURE__*/React.createElement(View, {
558
+ style: s.actionRow
559
+ }, step.actions.map((action, i) => /*#__PURE__*/React.createElement(ActionButton, {
560
+ key: i,
561
+ action: action,
562
+ btnPrimaryBg: step.buttonPrimaryBackgroundColor,
563
+ btnPrimaryText: step.buttonPrimaryTextColor,
564
+ btnGhostText: step.buttonGhostTextColor,
565
+ onPress: () => {
566
+ const isMultiStep = config.steps.length > 1;
567
+ const isLastStep = stepIndex === config.steps.length - 1;
568
+ const actionUrl = 'url' in action ? action.url : undefined;
569
+ request.onExperienceEvent({
570
+ type: 'clicked',
571
+ stepIndex,
572
+ stepTotal: config.steps.length,
573
+ anchorKey: step.anchorKey,
574
+ displayStyle: 'tooltip',
575
+ ctaLabel: action.label,
576
+ actionType: action.type,
577
+ actionUrl
578
+ });
579
+ if (isMultiStep) {
580
+ request.onExperienceEvent({
581
+ type: 'step_clicked',
582
+ stepIndex,
583
+ stepTotal: config.steps.length,
584
+ anchorKey: step.anchorKey,
585
+ displayStyle: 'tooltip',
586
+ ctaLabel: action.label,
587
+ actionType: action.type,
588
+ actionUrl
589
+ });
590
+ }
591
+ if (isMultiStep && isLastStep && action.type !== 'back') {
592
+ request.onExperienceEvent({
593
+ type: 'completed',
594
+ stepIndex,
595
+ stepTotal: config.steps.length,
596
+ anchorKey: step.anchorKey,
597
+ displayStyle: 'tooltip'
598
+ });
599
+ }
600
+ void digiaActionHandler.execute(action, {
601
+ campaign_id: request.payloadId,
602
+ campaign_key: request.campaignKey,
603
+ campaign_type: 'guide',
604
+ source: {
605
+ kind: 'button',
606
+ button_label: action.label
607
+ },
608
+ step_index: stepIndex,
609
+ step_total: config.steps.length
610
+ }, actionCallbacks());
611
+ }
612
+ })))) :
613
+ /*#__PURE__*/
614
+ // Off-screen measurement pass to determine bubble size before positioning.
615
+ React.createElement(View, {
616
+ pointerEvents: "none",
617
+ onLayout: e => setFloatingSize({
618
+ w: e.nativeEvent.layout.width,
619
+ h: e.nativeEvent.layout.height
620
+ }),
621
+ style: [s.tooltipBubble, {
622
+ left: -9999,
623
+ top: -9999,
624
+ width: tooltipW,
625
+ padding: step.padding
626
+ }]
627
+ }, /*#__PURE__*/React.createElement(Text, {
628
+ style: {
629
+ fontSize: step.titleSize,
630
+ fontFamily
631
+ }
632
+ }, step.title), !!step.body && /*#__PURE__*/React.createElement(Text, {
633
+ style: {
634
+ fontSize: step.bodySize,
635
+ fontFamily
636
+ }
637
+ }, step.body), /*#__PURE__*/React.createElement(View, {
638
+ style: s.actionRow
639
+ }, step.actions.map((a, i) => /*#__PURE__*/React.createElement(View, {
640
+ key: i,
641
+ style: s.button
642
+ }, /*#__PURE__*/React.createElement(Text, {
643
+ style: {
644
+ fontSize: 13,
645
+ fontFamily
646
+ }
647
+ }, a.label)))))));
648
+ }
649
+
650
+ // ─── Spotlight overlay ────────────────────────────────────────────────────────
651
+
652
+ function buildCutoutPath(x, y, w, h, radius, shape) {
653
+ if (shape === 'circle') {
654
+ const cx = x + w / 2;
655
+ const cy = y + h / 2;
656
+ const r = Math.max(w, h) / 2;
657
+ return `M${cx - r},${cy} a${r},${r} 0 1,0 ${r * 2},0 a${r},${r} 0 1,0 -${r * 2},0 Z`;
658
+ }
659
+ if (shape === 'pill') {
660
+ const r = h / 2;
661
+ return `M${x + r},${y} L${x + w - r},${y} Q${x + w},${y} ${x + w},${y + r} L${x + w},${y + h - r} Q${x + w},${y + h} ${x + w - r},${y + h} L${x + r},${y + h} Q${x},${y + h} ${x},${y + h - r} L${x},${y + r} Q${x},${y} ${x + r},${y} Z`;
662
+ }
663
+ const r = Math.max(0, radius);
664
+ if (r === 0) {
665
+ return `M${x},${y} L${x + w},${y} L${x + w},${y + h} L${x},${y + h} Z`;
666
+ }
667
+ return `M${x + r},${y} L${x + w - r},${y} Q${x + w},${y} ${x + w},${y + r} L${x + w},${y + h - r} Q${x + w},${y + h} ${x + w - r},${y + h} L${x + r},${y + h} Q${x},${y + h} ${x},${y + h - r} L${x},${y + r} Q${x},${y} ${x + r},${y} Z`;
668
+ }
669
+ function SpotlightCallout({
670
+ step,
671
+ layout,
672
+ onActionPress
673
+ }) {
674
+ const {
675
+ width: screenW
676
+ } = useWindowDimensions();
677
+ const [floatPos, setFloatPos] = useState(null);
678
+ const [resolvedPlacement, setResolvedPlacement] = useState('below');
679
+ const [floatingSize, setFloatingSize] = useState(null);
680
+ const calloutW = Math.min(step.calloutMaxWidth, screenW - 32);
681
+ const arrowSize = step.arrowSize ?? 8;
682
+ const showArrow = step.showArrow !== false;
683
+ const gap = (step.calloutGap ?? 8) + (showArrow ? arrowSize : 0);
684
+ const fontFamily = Digia.fontFamily;
685
+ useEffect(() => {
686
+ if (!floatingSize) return;
687
+ const fpPlacement = step.calloutPosition === 'above' ? 'top' : step.calloutPosition === 'below' ? 'bottom' : step.calloutPosition === 'auto' ? 'bottom' : step.calloutPosition;
688
+ computePosition(makeVirtualRef(layout, step.highlightPadding), {
689
+ w: Math.min(calloutW, floatingSize.w),
690
+ h: floatingSize.h
691
+ }, {
692
+ platform: rnCorePlatform,
693
+ placement: fpPlacement,
694
+ middleware: [offset(gap), flip(), shift({
695
+ padding: 16
696
+ })]
697
+ }).then(({
698
+ x,
699
+ y,
700
+ placement
701
+ }) => {
702
+ console.log('[Digia:spotlight] floatPos=', {
703
+ x,
704
+ y
705
+ }, 'resolved=', placement);
706
+ setFloatPos({
707
+ x,
708
+ y
709
+ });
710
+ setResolvedPlacement(placement);
711
+ });
712
+ }, [layout, floatingSize, step.calloutPosition, calloutW, step.highlightPadding, gap]);
713
+
714
+ // Compute arrow offset: point arrow tip at anchor center.
715
+ const arrowOffset = showArrow && floatPos && floatingSize ? calcArrowOffset(resolvedPlacement, layout, floatPos, calloutW, floatingSize.h, step.calloutCornerRadius, arrowSize) : undefined;
716
+ const calloutStyle = {
717
+ backgroundColor: step.calloutBackgroundColor,
718
+ borderRadius: step.calloutCornerRadius,
719
+ padding: step.calloutPadding,
720
+ borderWidth: step.calloutBorderWidth,
721
+ borderColor: step.calloutBorderColor,
722
+ width: calloutW
723
+ };
724
+ if (!floatPos) {
725
+ return /*#__PURE__*/React.createElement(View, {
726
+ pointerEvents: "none",
727
+ onLayout: e => setFloatingSize({
728
+ w: e.nativeEvent.layout.width,
729
+ h: e.nativeEvent.layout.height
730
+ }),
731
+ style: [calloutStyle, {
732
+ position: 'absolute',
733
+ left: -9999,
734
+ top: -9999
735
+ }]
736
+ }, /*#__PURE__*/React.createElement(Text, {
737
+ style: {
738
+ fontSize: step.titleSize,
739
+ fontFamily
740
+ }
741
+ }, step.title), !!step.body && /*#__PURE__*/React.createElement(Text, {
742
+ style: {
743
+ marginTop: 4,
744
+ fontSize: step.bodySize,
745
+ fontFamily
746
+ }
747
+ }, step.body), /*#__PURE__*/React.createElement(View, {
748
+ style: s.actionRow
749
+ }, step.actions.map((a, i) => /*#__PURE__*/React.createElement(View, {
750
+ key: i,
751
+ style: s.button
752
+ }, /*#__PURE__*/React.createElement(Text, {
753
+ style: {
754
+ fontSize: 13,
755
+ fontFamily
756
+ }
757
+ }, a.label)))));
758
+ }
759
+ return /*#__PURE__*/React.createElement(View, {
760
+ style: [calloutStyle, {
761
+ position: 'absolute',
762
+ left: floatPos.x,
763
+ top: floatPos.y
764
+ }, step.calloutShadow && s.shadow]
765
+ }, showArrow && /*#__PURE__*/React.createElement(GuideArrow, {
766
+ placement: resolvedPlacement,
767
+ color: step.arrowColor ?? step.calloutBackgroundColor,
768
+ borderColor: step.arrowBorderColor ?? step.calloutBorderColor,
769
+ size: arrowSize,
770
+ arrowOffset: arrowOffset
771
+ }), /*#__PURE__*/React.createElement(Text, {
772
+ style: {
773
+ color: step.titleColor,
774
+ fontSize: step.titleSize,
775
+ fontWeight: step.titleWeight,
776
+ fontFamily
777
+ }
778
+ }, step.title), !!step.body && /*#__PURE__*/React.createElement(Text, {
779
+ style: {
780
+ marginTop: 4,
781
+ color: step.bodyColor,
782
+ fontSize: step.bodySize,
783
+ fontFamily
784
+ }
785
+ }, step.body), /*#__PURE__*/React.createElement(View, {
786
+ style: s.actionRow
787
+ }, step.actions.map((action, i) => /*#__PURE__*/React.createElement(ActionButton, {
788
+ key: i,
789
+ action: action,
790
+ btnPrimaryBg: step.buttonPrimaryBackgroundColor,
791
+ btnPrimaryText: step.buttonPrimaryTextColor,
792
+ btnGhostText: step.buttonGhostTextColor,
793
+ onPress: () => {
794
+ onActionPress(action);
795
+ }
796
+ }))));
797
+ }
798
+ function SpotlightOverlay({
799
+ request,
800
+ config
801
+ }) {
802
+ const [stepIndex, setStepIndex] = useState(0);
803
+ const [layout, setLayout] = useState(null);
804
+ const step = config.steps[stepIndex];
805
+ const {
806
+ width: screenW,
807
+ height: screenH
808
+ } = useWindowDimensions();
809
+ const opacityAnim = useRef(new Animated.Value(1)).current;
810
+ const pendingFadeIn = useRef(false);
811
+ useEffect(() => {
812
+ setLayout(null);
813
+ return digiaAnchorRegistry.subscribe(step.anchorKey, l => {
814
+ setLayout(l);
815
+ });
816
+ }, [step.anchorKey]);
817
+ useEffect(() => {
818
+ if (layout && pendingFadeIn.current) {
819
+ pendingFadeIn.current = false;
820
+ Animated.timing(opacityAnim, {
821
+ toValue: 1,
822
+ duration: 180,
823
+ useNativeDriver: true
824
+ }).start();
825
+ }
826
+ }, [layout, opacityAnim]);
827
+
828
+ // Fire viewed/step_viewed when the step renders.
829
+ useEffect(() => {
830
+ const isMultiStep = config.steps.length > 1;
831
+ if (stepIndex === 0) {
832
+ request.onExperienceEvent({
833
+ type: 'viewed',
834
+ stepIndex: 0,
835
+ stepTotal: config.steps.length,
836
+ anchorKey: step.anchorKey,
837
+ displayStyle: 'spotlight'
838
+ });
839
+ }
840
+ if (isMultiStep) {
841
+ request.onExperienceEvent({
842
+ type: 'step_viewed',
843
+ stepIndex,
844
+ stepTotal: config.steps.length,
845
+ anchorKey: step.anchorKey,
846
+ displayStyle: 'spotlight'
847
+ });
848
+ }
849
+ // eslint-disable-next-line react-hooks/exhaustive-deps
850
+ }, [stepIndex]);
851
+ const closeFromCTA = useCallback(() => {
852
+ Animated.timing(opacityAnim, {
853
+ toValue: 0,
854
+ duration: 150,
855
+ useNativeDriver: true
856
+ }).start(() => {
857
+ digiaGuideController.cancel(request.payloadId);
858
+ });
859
+ }, [opacityAnim, request]);
860
+ const dismiss = useCallback((reason = 'scrim_tap') => {
861
+ Animated.timing(opacityAnim, {
862
+ toValue: 0,
863
+ duration: 150,
864
+ useNativeDriver: true
865
+ }).start(() => {
866
+ const isMultiStep = config.steps.length > 1;
867
+ request.onExperienceEvent({
868
+ type: 'dismissed',
869
+ stepIndex,
870
+ stepTotal: config.steps.length,
871
+ anchorKey: step.anchorKey,
872
+ displayStyle: 'spotlight',
873
+ dismissReason: reason
874
+ });
875
+ if (isMultiStep) {
876
+ request.onExperienceEvent({
877
+ type: 'step_dismissed',
878
+ stepIndex,
879
+ stepTotal: config.steps.length,
880
+ anchorKey: step.anchorKey,
881
+ displayStyle: 'spotlight',
882
+ dismissReason: reason
883
+ });
884
+ }
885
+ digiaGuideController.cancel(request.payloadId);
886
+ });
887
+ }, [request, opacityAnim, step, config, stepIndex]);
888
+ const stepTo = useCallback(newIndex => {
889
+ Animated.timing(opacityAnim, {
890
+ toValue: 0,
891
+ duration: 150,
892
+ useNativeDriver: true
893
+ }).start(() => {
894
+ if (newIndex === null) {
895
+ digiaGuideController.cancel(request.payloadId);
896
+ } else {
897
+ pendingFadeIn.current = true;
898
+ setStepIndex(newIndex);
899
+ }
900
+ });
901
+ }, [opacityAnim, request]);
902
+ const next = useCallback(() => stepTo(stepIndex < config.steps.length - 1 ? stepIndex + 1 : null), [stepIndex, config.steps.length, stepTo]);
903
+ const prev = useCallback(() => {
904
+ if (stepIndex > 0) stepTo(stepIndex - 1);
905
+ }, [stepIndex, stepTo]);
906
+ const actionCallbacks = useCallback(() => ({
907
+ onNext: next,
908
+ onBack: prev,
909
+ onDismissSelf: closeFromCTA,
910
+ onDismissAll: closeFromCTA
911
+ }), [next, prev, closeFromCTA]);
912
+ const handleActionPress = useCallback(action => {
913
+ const isMultiStep = config.steps.length > 1;
914
+ const isLastStep = stepIndex === config.steps.length - 1;
915
+ const actionUrl = 'url' in action ? action.url : undefined;
916
+ request.onExperienceEvent({
917
+ type: 'clicked',
918
+ stepIndex,
919
+ stepTotal: config.steps.length,
920
+ anchorKey: step.anchorKey,
921
+ displayStyle: 'spotlight',
922
+ ctaLabel: action.label,
923
+ actionType: action.type,
924
+ actionUrl
925
+ });
926
+ if (isMultiStep) {
927
+ request.onExperienceEvent({
928
+ type: 'step_clicked',
929
+ stepIndex,
930
+ stepTotal: config.steps.length,
931
+ anchorKey: step.anchorKey,
932
+ displayStyle: 'spotlight',
933
+ ctaLabel: action.label,
934
+ actionType: action.type,
935
+ actionUrl
936
+ });
937
+ }
938
+ if (isMultiStep && isLastStep && action.type !== 'back') {
939
+ request.onExperienceEvent({
940
+ type: 'completed',
941
+ stepIndex,
942
+ stepTotal: config.steps.length,
943
+ anchorKey: step.anchorKey,
944
+ displayStyle: 'spotlight'
945
+ });
946
+ }
947
+ digiaActionHandler.execute(action, {
948
+ campaign_id: request.payloadId,
949
+ campaign_key: request.campaignKey,
950
+ campaign_type: 'guide',
951
+ source: {
952
+ kind: 'button',
953
+ button_label: action.label
954
+ },
955
+ step_index: stepIndex,
956
+ step_total: config.steps.length
957
+ }, actionCallbacks());
958
+ }, [request, stepIndex, config, step, actionCallbacks]);
959
+ const handleBackdropPress = useCallback(() => {
960
+ const behavior = config.outsideTapBehavior ?? 'next';
961
+ if (behavior === 'nothing') return;
962
+ if (behavior === 'next') next();
963
+ if (behavior === 'dismiss') dismiss('scrim_tap');
964
+ }, [config.outsideTapBehavior, next, dismiss]);
965
+ const pad = step.highlightPadding;
966
+ const cutoutX = layout ? layout.pageX - pad : 0;
967
+ const cutoutY = layout ? layout.pageY - pad : 0;
968
+ const cutoutW = layout ? layout.width + pad * 2 : 0;
969
+ const cutoutH = layout ? layout.height + pad * 2 : 0;
970
+ const screenPath = `M0,0 L${screenW},0 L${screenW},${screenH} L0,${screenH} Z`;
971
+ const cutoutPath = layout ? buildCutoutPath(cutoutX, cutoutY, cutoutW, cutoutH, step.highlightCornerRadius, step.highlightShape) : '';
972
+ return /*#__PURE__*/React.createElement(Modal, {
973
+ transparent: true,
974
+ statusBarTranslucent: true,
975
+ animationType: "none",
976
+ visible: true
977
+ }, /*#__PURE__*/React.createElement(Animated.View, {
978
+ style: [StyleSheet.absoluteFill, {
979
+ opacity: opacityAnim
980
+ }],
981
+ pointerEvents: "box-none"
982
+ }, layout && /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Svg, {
983
+ width: screenW,
984
+ height: screenH,
985
+ style: StyleSheet.absoluteFill,
986
+ pointerEvents: "none"
987
+ }, /*#__PURE__*/React.createElement(Path, {
988
+ fillRule: "evenodd",
989
+ d: `${screenPath} ${cutoutPath}`,
990
+ fill: step.overlayColor,
991
+ fillOpacity: step.overlayOpacity
992
+ }), step.highlightGlowWidth > 0 && /*#__PURE__*/React.createElement(Path, {
993
+ d: cutoutPath,
994
+ fill: "none",
995
+ stroke: step.highlightGlowColor,
996
+ strokeWidth: step.highlightGlowWidth
997
+ })), /*#__PURE__*/React.createElement(Pressable, {
998
+ style: StyleSheet.absoluteFill,
999
+ onPress: handleBackdropPress
1000
+ }), /*#__PURE__*/React.createElement(SpotlightCallout, {
1001
+ step: step,
1002
+ layout: layout,
1003
+ onActionPress: handleActionPress
1004
+ }))));
1005
+ }
1006
+
1007
+ // ─── Guide runtime dispatcher ─────────────────────────────────────────────────
1008
+
1009
+ function DigiaGuideRuntime() {
1010
+ const [activeRequest, setActiveRequest] = useState(null);
1011
+ useEffect(() => {
1012
+ return digiaGuideController.subscribe(event => {
1013
+ if (event.type === 'cancel') {
1014
+ setActiveRequest(null);
1015
+ return;
1016
+ }
1017
+ setActiveRequest(event.request);
1018
+ });
1019
+ }, []);
1020
+ if (!activeRequest) return null;
1021
+ switch (activeRequest.config.templateType) {
1022
+ case 'tooltip':
1023
+ return /*#__PURE__*/React.createElement(TooltipOverlay, {
1024
+ request: activeRequest,
1025
+ config: activeRequest.config
1026
+ });
1027
+ case 'spotlight':
1028
+ return /*#__PURE__*/React.createElement(SpotlightOverlay, {
1029
+ request: activeRequest,
1030
+ config: activeRequest.config
1031
+ });
1032
+ }
1033
+ }
1034
+
1035
+ // ─── DigiaHost ────────────────────────────────────────────────────────────────
1036
+
1037
+ export function DigiaHost() {
1038
+ return /*#__PURE__*/React.createElement(DigiaGuideRuntime, null);
1039
+ }
1040
+
1041
+ // ─── Styles ───────────────────────────────────────────────────────────────────
1042
+
1043
+ const s = StyleSheet.create({
1044
+ tooltipBubble: {
1045
+ position: 'absolute'
1046
+ },
1047
+ shadow: {
1048
+ shadowColor: '#000',
1049
+ shadowOpacity: 0.15,
1050
+ shadowRadius: 12,
1051
+ shadowOffset: {
1052
+ width: 0,
1053
+ height: 4
1054
+ },
1055
+ elevation: 8
1056
+ },
1057
+ actionRow: {
1058
+ marginTop: 12,
1059
+ flexDirection: 'row',
1060
+ justifyContent: 'flex-end',
1061
+ gap: 8
1062
+ },
1063
+ button: {
1064
+ minHeight: 32,
1065
+ minWidth: 60,
1066
+ alignItems: 'center',
1067
+ justifyContent: 'center',
1068
+ borderRadius: 8,
1069
+ paddingHorizontal: 12
1070
+ }
1071
+ });
1072
+ //# sourceMappingURL=DigiaProvider.js.map