@digia-engage/core 2.1.0 → 2.2.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 (54) hide show
  1. package/DigiaEngageReactNative.podspec +1 -1
  2. package/android/.project +0 -6
  3. package/android/bin/.gradle/8.13/fileHashes/fileHashes.lock +0 -0
  4. package/android/bin/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  5. package/android/bin/.gradle/buildOutputCleanup/cache.properties +2 -0
  6. package/android/bin/.project +34 -0
  7. package/android/bin/build/generated/source/buildConfig/debug/com/digia/engage/rn/BuildConfig.class +0 -0
  8. package/android/bin/build/generated/source/codegen/java/com/digia/engage/rn/NativeDigiaEngageSpec.class +0 -0
  9. package/android/bin/build.gradle +97 -0
  10. package/android/bin/gradle/wrapper/gradle-wrapper.jar +0 -0
  11. package/android/bin/gradle/wrapper/gradle-wrapper.properties +5 -0
  12. package/android/bin/gradle.properties +2 -0
  13. package/android/bin/gradlew +185 -0
  14. package/android/bin/gradlew.bat +89 -0
  15. package/android/bin/local.properties +1 -0
  16. package/android/bin/settings.gradle +25 -0
  17. package/android/bin/src/main/AndroidManifest.xml +2 -0
  18. package/android/bin/src/main/java/com/digia/engage/rn/DigiaAnchorViewManager.kt +90 -0
  19. package/android/bin/src/main/java/com/digia/engage/rn/DigiaModule.kt +309 -0
  20. package/android/bin/src/main/java/com/digia/engage/rn/DigiaPackage.kt +70 -0
  21. package/android/bin/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +183 -0
  22. package/android/bin/src/main/java/com/digia/engage/rn/DigiaViewManager.kt +64 -0
  23. package/android/build.gradle +1 -1
  24. package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +37 -13
  25. package/ios/DigiaEngageModule.m +25 -26
  26. package/ios/DigiaModule.swift +70 -11
  27. package/lib/commonjs/Digia.js +13 -2
  28. package/lib/commonjs/Digia.js.map +1 -1
  29. package/lib/commonjs/DigiaGuideController.js.map +1 -1
  30. package/lib/commonjs/DigiaProvider.js +38 -22
  31. package/lib/commonjs/DigiaProvider.js.map +1 -1
  32. package/lib/commonjs/interpolate.js +41 -0
  33. package/lib/commonjs/interpolate.js.map +1 -0
  34. package/lib/module/Digia.js +13 -2
  35. package/lib/module/Digia.js.map +1 -1
  36. package/lib/module/DigiaGuideController.js.map +1 -1
  37. package/lib/module/DigiaProvider.js +40 -23
  38. package/lib/module/DigiaProvider.js.map +1 -1
  39. package/lib/module/interpolate.js +34 -0
  40. package/lib/module/interpolate.js.map +1 -0
  41. package/lib/typescript/Digia.d.ts +1 -0
  42. package/lib/typescript/Digia.d.ts.map +1 -1
  43. package/lib/typescript/DigiaGuideController.d.ts +2 -0
  44. package/lib/typescript/DigiaGuideController.d.ts.map +1 -1
  45. package/lib/typescript/DigiaProvider.d.ts.map +1 -1
  46. package/lib/typescript/interpolate.d.ts +4 -0
  47. package/lib/typescript/interpolate.d.ts.map +1 -0
  48. package/package.json +1 -1
  49. package/react-native.config.js +1 -1
  50. package/src/Digia.ts +15 -2
  51. package/src/DigiaAnchorView.tsx +1 -0
  52. package/src/DigiaGuideController.ts +2 -0
  53. package/src/DigiaProvider.tsx +160 -140
  54. package/src/interpolate.ts +44 -0
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
1
+ import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
2
2
  import {
3
3
  Animated,
4
4
  Dimensions,
@@ -17,6 +17,18 @@ import { digiaAnchorRegistry, type AnchorLayout } from './digiaAnchorRegistry';
17
17
  import { digiaActionHandler, type ActionCallbacks } from './actionHandler';
18
18
  import type { DismissReason } from './types';
19
19
  import type { Action, SpotlightConfig, SpotlightStep, TooltipConfig, TooltipStep } from './templateTypes';
20
+ import { interpolateVariables, type VariableMap } from './interpolate';
21
+
22
+ // ─── Variable context ─────────────────────────────────────────────────────────
23
+ // Provides the active campaign's variable map to all descendant components.
24
+ // Avoids threading `variables` through every prop chain.
25
+
26
+ const VariableContext = createContext<VariableMap | undefined>(undefined);
27
+
28
+ function TextWithVariables({ children, ...props }: Omit<React.ComponentProps<typeof Text>, 'children'> & { children: string }) {
29
+ const variables = useContext(VariableContext);
30
+ return <Text {...props}>{interpolateVariables(children, variables)}</Text>;
31
+ }
20
32
 
21
33
  // ─── @floating-ui/core platform adapter ──────────────────────────────────────
22
34
 
@@ -184,9 +196,9 @@ function ActionButton({
184
196
  onPress={onPress}
185
197
  style={[s.button, isPrimary && { backgroundColor: btnPrimaryBg }]}
186
198
  >
187
- <Text style={{ color: isPrimary ? btnPrimaryText : btnGhostText, fontSize: 13, fontWeight: '600', fontFamily }}>
199
+ <TextWithVariables style={{ color: isPrimary ? btnPrimaryText : btnGhostText, fontSize: 13, fontWeight: '600', fontFamily }}>
188
200
  {action.label}
189
- </Text>
201
+ </TextWithVariables>
190
202
  </Pressable>
191
203
  );
192
204
  }
@@ -327,104 +339,110 @@ function TooltipOverlay({
327
339
  }, [config.outsideTapBehavior, next, dismiss]);
328
340
 
329
341
  return (
330
- <Modal transparent statusBarTranslucent animationType="none" visible>
331
- <Animated.View style={[StyleSheet.absoluteFill, { opacity: opacityAnim }]}>
332
- {/* Full-screen backdrop: blocks all touches (scroll, tap) */}
333
- <Pressable style={StyleSheet.absoluteFill} onPress={handleBackdropPress} />
334
- {floatPos ? (
335
- // Bubble as Pressable so tapping the bubble body also advances
336
- <Pressable
337
- onLayout={(e) => {
338
- const { width, height } = e.nativeEvent.layout;
339
- if (floatingSize?.w !== width || floatingSize?.h !== height) {
340
- setFloatingSize({ w: width, h: height });
341
- }
342
- }}
343
- onPress={handleBackdropPress}
344
- style={[
345
- s.tooltipBubble,
346
- {
347
- left: floatPos.x,
348
- top: floatPos.y,
349
- width: tooltipW,
350
- backgroundColor: step.backgroundColor,
351
- borderRadius: step.cornerRadius,
352
- borderWidth: step.borderWidth,
353
- borderColor: step.borderColor,
354
- padding: step.padding,
355
- },
356
- step.shadow && s.shadow,
357
- ]}
358
- >
359
- {showArrow && (
360
- <GuideArrow
361
- placement={resolvedPlacement}
362
- color={step.arrowColor ?? step.backgroundColor}
363
- borderColor={step.arrowBorderColor ?? step.borderColor}
364
- size={arrowSize}
365
- arrowOffset={arrowOffset}
366
- />
367
- )}
368
- <Text style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
369
- {step.title}
370
- </Text>
371
- {!!step.body && (
372
- <Text style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
373
- {step.body}
374
- </Text>
375
- )}
376
- <View style={s.actionRow}>
377
- {step.actions.map((action, i) => (
378
- <ActionButton
379
- key={i}
380
- action={action}
381
- btnPrimaryBg={step.buttonPrimaryBackgroundColor}
382
- btnPrimaryText={step.buttonPrimaryTextColor}
383
- btnGhostText={step.buttonGhostTextColor}
384
- onPress={() => {
385
- const isMultiStep = config.steps.length > 1;
386
- const isLastStep = stepIndex === config.steps.length - 1;
387
- const actionUrl = 'url' in action ? (action as { url: string }).url : undefined;
388
- request.onExperienceEvent({ type: 'clicked', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip', ctaLabel: action.label, actionType: action.type, actionUrl });
389
- if (isMultiStep) {
390
- request.onExperienceEvent({ type: 'step_clicked', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip', ctaLabel: action.label, actionType: action.type, actionUrl });
391
- }
392
- if (isMultiStep && isLastStep && action.type !== 'back') {
393
- request.onExperienceEvent({ type: 'completed', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip' });
394
- }
395
- void digiaActionHandler.execute(action, {
396
- campaign_id: request.payloadId,
397
- campaign_key: request.campaignKey,
398
- campaign_type: 'guide',
399
- source: { kind: 'button', button_label: action.label },
400
- step_index: stepIndex,
401
- step_total: config.steps.length,
402
- }, actionCallbacks());
403
- }}
404
- />
405
- ))}
406
- </View>
407
- </Pressable>
408
- ) : (
409
- // Off-screen measurement pass to determine bubble size before positioning.
410
- <View
411
- pointerEvents="none"
412
- onLayout={(e) => setFloatingSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
413
- style={[s.tooltipBubble, { left: -9999, top: -9999, width: tooltipW, padding: step.padding }]}
414
- >
415
- <Text style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</Text>
416
- {!!step.body && <Text style={{ fontSize: step.bodySize, fontFamily }}>{step.body}</Text>}
417
- <View style={s.actionRow}>
418
- {step.actions.map((a, i) => (
419
- <View key={i} style={s.button}>
420
- <Text style={{ fontSize: 13, fontFamily }}>{a.label}</Text>
342
+ <VariableContext.Provider value={request.variables}>
343
+ <Modal transparent statusBarTranslucent animationType="none" visible>
344
+ {/* pointerEvents="box-none": container passes touches through; only children intercept.
345
+ This prevents the invisible measurement pass from blocking the screen. */}
346
+ <Animated.View style={[StyleSheet.absoluteFill, { opacity: opacityAnim }]} pointerEvents="box-none">
347
+ {floatPos ? (
348
+ <>
349
+ {/* Full-screen backdrop: blocks all touches once bubble is positioned */}
350
+ <Pressable style={StyleSheet.absoluteFill} onPress={handleBackdropPress} />
351
+ {/* Bubble as Pressable so tapping the bubble body also advances */}
352
+ <Pressable
353
+ onLayout={(e) => {
354
+ const { width, height } = e.nativeEvent.layout;
355
+ if (floatingSize?.w !== width || floatingSize?.h !== height) {
356
+ setFloatingSize({ w: width, h: height });
357
+ }
358
+ }}
359
+ onPress={handleBackdropPress}
360
+ style={[
361
+ s.tooltipBubble,
362
+ {
363
+ left: floatPos.x,
364
+ top: floatPos.y,
365
+ width: tooltipW,
366
+ backgroundColor: step.backgroundColor,
367
+ borderRadius: step.cornerRadius,
368
+ borderWidth: step.borderWidth,
369
+ borderColor: step.borderColor,
370
+ padding: step.padding,
371
+ },
372
+ step.shadow && s.shadow,
373
+ ]}
374
+ >
375
+ {showArrow && (
376
+ <GuideArrow
377
+ placement={resolvedPlacement}
378
+ color={step.arrowColor ?? step.backgroundColor}
379
+ borderColor={step.arrowBorderColor ?? step.borderColor}
380
+ size={arrowSize}
381
+ arrowOffset={arrowOffset}
382
+ />
383
+ )}
384
+ <TextWithVariables style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
385
+ {step.title}
386
+ </TextWithVariables>
387
+ {!!step.body && (
388
+ <TextWithVariables style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
389
+ {step.body}
390
+ </TextWithVariables>
391
+ )}
392
+ <View style={s.actionRow}>
393
+ {step.actions.map((action, i) => (
394
+ <ActionButton
395
+ key={i}
396
+ action={action}
397
+ btnPrimaryBg={step.buttonPrimaryBackgroundColor}
398
+ btnPrimaryText={step.buttonPrimaryTextColor}
399
+ btnGhostText={step.buttonGhostTextColor}
400
+ onPress={() => {
401
+ const isMultiStep = config.steps.length > 1;
402
+ const isLastStep = stepIndex === config.steps.length - 1;
403
+ const actionUrl = 'url' in action ? (action as { url: string }).url : undefined;
404
+ request.onExperienceEvent({ type: 'clicked', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip', ctaLabel: action.label, actionType: action.type, actionUrl });
405
+ if (isMultiStep) {
406
+ request.onExperienceEvent({ type: 'step_clicked', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip', ctaLabel: action.label, actionType: action.type, actionUrl });
407
+ }
408
+ if (isMultiStep && isLastStep && action.type !== 'back') {
409
+ request.onExperienceEvent({ type: 'completed', stepIndex, stepTotal: config.steps.length, anchorKey: step.anchorKey, displayStyle: 'tooltip' });
410
+ }
411
+ void digiaActionHandler.execute(action, {
412
+ campaign_id: request.payloadId,
413
+ campaign_key: request.campaignKey,
414
+ campaign_type: 'guide',
415
+ source: { kind: 'button', button_label: action.label },
416
+ step_index: stepIndex,
417
+ step_total: config.steps.length,
418
+ }, actionCallbacks());
419
+ }}
420
+ />
421
+ ))}
421
422
  </View>
422
- ))}
423
+ </Pressable>
424
+ </>
425
+ ) : (
426
+ // Off-screen measurement pass to determine bubble size before positioning.
427
+ <View
428
+ pointerEvents="none"
429
+ onLayout={(e) => setFloatingSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
430
+ style={[s.tooltipBubble, { left: -9999, top: -9999, width: tooltipW, padding: step.padding }]}
431
+ >
432
+ <TextWithVariables style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</TextWithVariables>
433
+ {!!step.body && <TextWithVariables style={{ fontSize: step.bodySize, fontFamily }}>{step.body}</TextWithVariables>}
434
+ <View style={s.actionRow}>
435
+ {step.actions.map((a, i) => (
436
+ <View key={i} style={s.button}>
437
+ <TextWithVariables style={{ fontSize: 13, fontFamily }}>{a.label}</TextWithVariables>
438
+ </View>
439
+ ))}
440
+ </View>
423
441
  </View>
424
- </View>
425
- )}
426
- </Animated.View>
427
- </Modal>
442
+ )}
443
+ </Animated.View>
444
+ </Modal>
445
+ </VariableContext.Provider>
428
446
  );
429
447
  }
430
448
 
@@ -515,12 +533,12 @@ function SpotlightCallout({
515
533
  onLayout={(e) => setFloatingSize({ w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height })}
516
534
  style={[calloutStyle, { position: 'absolute', left: -9999, top: -9999 }]}
517
535
  >
518
- <Text style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</Text>
519
- {!!step.body && <Text style={{ marginTop: 4, fontSize: step.bodySize, fontFamily }}>{step.body}</Text>}
536
+ <TextWithVariables style={{ fontSize: step.titleSize, fontFamily }}>{step.title}</TextWithVariables>
537
+ {!!step.body && <TextWithVariables style={{ marginTop: 4, fontSize: step.bodySize, fontFamily }}>{step.body}</TextWithVariables>}
520
538
  <View style={s.actionRow}>
521
539
  {step.actions.map((a, i) => (
522
540
  <View key={i} style={s.button}>
523
- <Text style={{ fontSize: 13, fontFamily }}>{a.label}</Text>
541
+ <TextWithVariables style={{ fontSize: 13, fontFamily }}>{a.label}</TextWithVariables>
524
542
  </View>
525
543
  ))}
526
544
  </View>
@@ -545,13 +563,13 @@ function SpotlightCallout({
545
563
  arrowOffset={arrowOffset}
546
564
  />
547
565
  )}
548
- <Text style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
566
+ <TextWithVariables style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
549
567
  {step.title}
550
- </Text>
568
+ </TextWithVariables>
551
569
  {!!step.body && (
552
- <Text style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
570
+ <TextWithVariables style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
553
571
  {step.body}
554
- </Text>
572
+ </TextWithVariables>
555
573
  )}
556
574
  <View style={s.actionRow}>
557
575
  {step.actions.map((action, i) => (
@@ -696,42 +714,44 @@ function SpotlightOverlay({
696
714
  : '';
697
715
 
698
716
  return (
699
- <Modal transparent statusBarTranslucent animationType="none" visible>
700
- <Animated.View style={[StyleSheet.absoluteFill, { opacity: opacityAnim }]} pointerEvents="box-none">
701
- {layout && (
702
- <>
703
- <Svg
704
- width={screenW}
705
- height={screenH}
706
- style={StyleSheet.absoluteFill}
707
- pointerEvents="none"
708
- >
709
- <Path
710
- fillRule="evenodd"
711
- d={`${screenPath} ${cutoutPath}`}
712
- fill={step.overlayColor}
713
- fillOpacity={step.overlayOpacity}
714
- />
715
- {step.highlightGlowWidth > 0 && (
717
+ <VariableContext.Provider value={request.variables}>
718
+ <Modal transparent statusBarTranslucent animationType="none" visible>
719
+ <Animated.View style={[StyleSheet.absoluteFill, { opacity: opacityAnim }]} pointerEvents="box-none">
720
+ {layout && (
721
+ <>
722
+ <Svg
723
+ width={screenW}
724
+ height={screenH}
725
+ style={StyleSheet.absoluteFill}
726
+ pointerEvents="none"
727
+ >
716
728
  <Path
717
- d={cutoutPath}
718
- fill="none"
719
- stroke={step.highlightGlowColor}
720
- strokeWidth={step.highlightGlowWidth}
729
+ fillRule="evenodd"
730
+ d={`${screenPath} ${cutoutPath}`}
731
+ fill={step.overlayColor}
732
+ fillOpacity={step.overlayOpacity}
721
733
  />
722
- )}
723
- </Svg>
724
- {/* Backdrop with configurable outside-tap behaviour */}
725
- <Pressable style={StyleSheet.absoluteFill} onPress={handleBackdropPress} />
726
- <SpotlightCallout
727
- step={step}
728
- layout={layout}
729
- onActionPress={handleActionPress}
730
- />
731
- </>
732
- )}
733
- </Animated.View>
734
- </Modal>
734
+ {step.highlightGlowWidth > 0 && (
735
+ <Path
736
+ d={cutoutPath}
737
+ fill="none"
738
+ stroke={step.highlightGlowColor}
739
+ strokeWidth={step.highlightGlowWidth}
740
+ />
741
+ )}
742
+ </Svg>
743
+ {/* Backdrop with configurable outside-tap behaviour */}
744
+ <Pressable style={StyleSheet.absoluteFill} onPress={handleBackdropPress} />
745
+ <SpotlightCallout
746
+ step={step}
747
+ layout={layout}
748
+ onActionPress={handleActionPress}
749
+ />
750
+ </>
751
+ )}
752
+ </Animated.View>
753
+ </Modal>
754
+ </VariableContext.Provider>
735
755
  );
736
756
  }
737
757
 
@@ -0,0 +1,44 @@
1
+ export type VariableMap = Record<string, string>;
2
+
3
+ const PLACEHOLDER_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
4
+
5
+ export function interpolateVariables(
6
+ text: string,
7
+ variables: VariableMap | undefined,
8
+ ): string {
9
+ if (!variables) return text;
10
+ return text.replace(PLACEHOLDER_PATTERN, (match, name: string) => {
11
+ const value = variables[name];
12
+ return value === undefined ? '' : value;
13
+ });
14
+ }
15
+
16
+ export function parseVariableMap(raw: unknown): VariableMap | undefined {
17
+ const source = parseVariableSource(raw);
18
+ if (!source) return undefined;
19
+
20
+ const variables: VariableMap = {};
21
+ for (const [key, value] of Object.entries(source)) {
22
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
23
+ if (typeof value === 'string') variables[key] = value;
24
+ else if (typeof value === 'number' || typeof value === 'boolean') variables[key] = String(value);
25
+ }
26
+ return Object.keys(variables).length > 0 ? variables : undefined;
27
+ }
28
+
29
+ function parseVariableSource(raw: unknown): Record<string, unknown> | undefined {
30
+ if (!raw) return undefined;
31
+ if (typeof raw === 'string') {
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ return isPlainObject(parsed) ? parsed : undefined;
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ return isPlainObject(raw) ? raw : undefined;
40
+ }
41
+
42
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
43
+ return !!value && typeof value === 'object' && !Array.isArray(value);
44
+ }