@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.
- package/DigiaEngageReactNative.podspec +1 -1
- package/android/.project +0 -6
- package/android/bin/.gradle/8.13/fileHashes/fileHashes.lock +0 -0
- package/android/bin/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/bin/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/bin/.project +34 -0
- package/android/bin/build/generated/source/buildConfig/debug/com/digia/engage/rn/BuildConfig.class +0 -0
- package/android/bin/build/generated/source/codegen/java/com/digia/engage/rn/NativeDigiaEngageSpec.class +0 -0
- package/android/bin/build.gradle +97 -0
- package/android/bin/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/bin/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/android/bin/gradle.properties +2 -0
- package/android/bin/gradlew +185 -0
- package/android/bin/gradlew.bat +89 -0
- package/android/bin/local.properties +1 -0
- package/android/bin/settings.gradle +25 -0
- package/android/bin/src/main/AndroidManifest.xml +2 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaAnchorViewManager.kt +90 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaModule.kt +309 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaPackage.kt +70 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaSlotViewManager.kt +183 -0
- package/android/bin/src/main/java/com/digia/engage/rn/DigiaViewManager.kt +64 -0
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/digia/engage/rn/DigiaModule.kt +37 -13
- package/ios/DigiaEngageModule.m +25 -26
- package/ios/DigiaModule.swift +70 -11
- package/lib/commonjs/Digia.js +13 -2
- package/lib/commonjs/Digia.js.map +1 -1
- package/lib/commonjs/DigiaGuideController.js.map +1 -1
- package/lib/commonjs/DigiaProvider.js +38 -22
- package/lib/commonjs/DigiaProvider.js.map +1 -1
- package/lib/commonjs/interpolate.js +41 -0
- package/lib/commonjs/interpolate.js.map +1 -0
- package/lib/module/Digia.js +13 -2
- package/lib/module/Digia.js.map +1 -1
- package/lib/module/DigiaGuideController.js.map +1 -1
- package/lib/module/DigiaProvider.js +40 -23
- package/lib/module/DigiaProvider.js.map +1 -1
- package/lib/module/interpolate.js +34 -0
- package/lib/module/interpolate.js.map +1 -0
- package/lib/typescript/Digia.d.ts +1 -0
- package/lib/typescript/Digia.d.ts.map +1 -1
- package/lib/typescript/DigiaGuideController.d.ts +2 -0
- package/lib/typescript/DigiaGuideController.d.ts.map +1 -1
- package/lib/typescript/DigiaProvider.d.ts.map +1 -1
- package/lib/typescript/interpolate.d.ts +4 -0
- package/lib/typescript/interpolate.d.ts.map +1 -0
- package/package.json +1 -1
- package/react-native.config.js +1 -1
- package/src/Digia.ts +15 -2
- package/src/DigiaAnchorView.tsx +1 -0
- package/src/DigiaGuideController.ts +2 -0
- package/src/DigiaProvider.tsx +160 -140
- package/src/interpolate.ts +44 -0
package/src/DigiaProvider.tsx
CHANGED
|
@@ -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
|
-
<
|
|
199
|
+
<TextWithVariables style={{ color: isPrimary ? btnPrimaryText : btnGhostText, fontSize: 13, fontWeight: '600', fontFamily }}>
|
|
188
200
|
{action.label}
|
|
189
|
-
</
|
|
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
|
-
<
|
|
331
|
-
<
|
|
332
|
-
{/*
|
|
333
|
-
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
</
|
|
427
|
-
</
|
|
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
|
-
<
|
|
519
|
-
{!!step.body && <
|
|
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
|
-
<
|
|
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
|
-
<
|
|
566
|
+
<TextWithVariables style={{ color: step.titleColor, fontSize: step.titleSize, fontWeight: step.titleWeight, fontFamily }}>
|
|
549
567
|
{step.title}
|
|
550
|
-
</
|
|
568
|
+
</TextWithVariables>
|
|
551
569
|
{!!step.body && (
|
|
552
|
-
<
|
|
570
|
+
<TextWithVariables style={{ marginTop: 4, color: step.bodyColor, fontSize: step.bodySize, fontFamily }}>
|
|
553
571
|
{step.body}
|
|
554
|
-
</
|
|
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
|
-
<
|
|
700
|
-
<
|
|
701
|
-
{
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
729
|
+
fillRule="evenodd"
|
|
730
|
+
d={`${screenPath} ${cutoutPath}`}
|
|
731
|
+
fill={step.overlayColor}
|
|
732
|
+
fillOpacity={step.overlayOpacity}
|
|
721
733
|
/>
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
+
}
|