rn-smart-tour 1.0.4 → 1.0.7

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 CHANGED
@@ -14,6 +14,7 @@
14
14
  - ⚡ **Auto-Start Engine**: Trigger tours instantly on mount with a smart debounce for layout stability.
15
15
  - 💾 **Seen State Caching**: Persistent "only-once" logic with pluggable storage (AsyncStorage, MMKV, etc.).
16
16
  - 🎨 **Smart Overlays**: Dynamic cutouts with Back/Next/Skip navigation and step indicators.
17
+ - 📜 **Auto-Scroll Support**: Seamlessly bring tour targets into view with the new `DapScrollView` integration.
17
18
 
18
19
  ---
19
20
 
@@ -77,6 +78,20 @@ const { startTour } = useDap();
77
78
  <Button title="Help" onPress={() => startTour('welcome-tour')} />
78
79
  ```
79
80
 
81
+ ### 4. Enable Auto-Scroll (Optional)
82
+ If your targets are hidden inside a long list, swap your standard `ScrollView` for `DapScrollView`. The tour will automatically scroll to bring each target into the user's view.
83
+
84
+ ```tsx
85
+ import { DapScrollView } from 'rn-smart-tour';
86
+
87
+ // ... inside your screen
88
+ <DapScrollView>
89
+ <DapTarget name="bottom-btn">
90
+ <Button title="Secret Button" ... />
91
+ </DapTarget>
92
+ </DapScrollView>
93
+ ```
94
+
80
95
  ---
81
96
 
82
97
  ## 🧠 Technical Architecture
@@ -112,6 +127,20 @@ When `autoStart: true` is enabled, the overlay waits **300ms** after registratio
112
127
  | `autoStart` | `boolean` | Trigger as soon as the first target mounts. |
113
128
  | `steps` | `TourStep[]` | Sequence of highlight steps. |
114
129
 
130
+ ### DapScrollView
131
+ A direct drop-in replacement for the React Native `ScrollView`. Supports all standard props.
132
+ | Property | Type | Description |
133
+ |:---|:---|:---|
134
+ | `children` | `React.Node` | Components to scroll. |
135
+ | `...props` | `ScrollViewProps` | Standard props are forwarded to the native ScrollView. |
136
+
137
+ ### DapTarget
138
+ | Property | Type | Description |
139
+ |:---|:---|:---|
140
+ | `name` | `string` | Unique identifier that matches a `targetId` in a tour step. |
141
+ | `children` | `ReactElement` | The UI element to wrap and highlight. |
142
+ | `...props` | `ViewProps` | All standard React Native `View` props are forwarded. |
143
+
115
144
  ### TourStep
116
145
  | Property | Type | Default | Description |
117
146
  |:---|:---|:---|:---|
@@ -126,9 +126,9 @@ const DapOverlay = () => {
126
126
  </react_native_1.Text>)}
127
127
 
128
128
  <react_native_1.View style={styles.actions}>
129
- <react_native_1.TouchableOpacity onPress={() => stopTour()} style={styles.actionBtn}>
130
- <react_native_1.Text style={styles.actionText}>Skip</react_native_1.Text>
131
- </react_native_1.TouchableOpacity>
129
+ {!isLastStep && (<react_native_1.TouchableOpacity onPress={() => stopTour()} style={styles.actionBtn}>
130
+ <react_native_1.Text style={styles.actionText}>Skip</react_native_1.Text>
131
+ </react_native_1.TouchableOpacity>)}
132
132
 
133
133
  {!isFirstStep && (<react_native_1.TouchableOpacity onPress={prevStep} style={styles.actionBtn}>
134
134
  <react_native_1.Text style={styles.actionText}>Back</react_native_1.Text>
@@ -35,14 +35,10 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.DapProvider = void 0;
37
37
  const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
38
39
  const DapContext_1 = require("./DapContext");
39
40
  const DapOverlay_1 = require("./DapOverlay");
40
41
  const STORAGE_KEY = '@rn-dap:seen_tours';
41
- /**
42
- * Debounce delay (ms) before auto-starting a tour after a target registers.
43
- * This gives the multi-pass measurement in DapTarget time to settle on the
44
- * final coordinates before the overlay is shown.
45
- */
46
42
  const AUTO_START_DEBOUNCE_MS = 300;
47
43
  const DapProvider = ({ children, tours, storageAdapter }) => {
48
44
  const [targets, setTargets] = (0, react_1.useState)({});
@@ -50,9 +46,13 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
50
46
  const [currentStepIndex, setCurrentStepIndex] = (0, react_1.useState)(0);
51
47
  const [seenTours, setSeenTours] = (0, react_1.useState)({});
52
48
  const [isStorageLoaded, setIsStorageLoaded] = (0, react_1.useState)(!storageAdapter);
53
- // Ref to avoid stale closures — always holds the latest activeTourId.
54
49
  const activeTourIdRef = (0, react_1.useRef)(activeTourId);
55
50
  activeTourIdRef.current = activeTourId;
51
+ const tourIdJustStoppedRef = (0, react_1.useRef)(null);
52
+ /** Map of target IDs to their native component references (for measureLayout). */
53
+ const targetRefs = (0, react_1.useRef)({});
54
+ /** Reference to the active ScrollView (if any) for auto-scrolling. */
55
+ const scrollRef = (0, react_1.useRef)(null);
56
56
  // Load seen tours on mount if a storage adapter is provided
57
57
  (0, react_1.useEffect)(() => {
58
58
  const loadStorage = async () => {
@@ -75,7 +75,6 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
75
75
  setSeenTours(prev => {
76
76
  const nextSeen = { ...prev, [tourId]: true };
77
77
  if (storageAdapter) {
78
- // Safe side-effect here for async storage, as long as it's fire-and-forget
79
78
  Promise.resolve(storageAdapter.setItem(STORAGE_KEY, JSON.stringify(nextSeen))).catch(e => {
80
79
  console.error('[rn-dap] failed to save storage', e);
81
80
  });
@@ -83,8 +82,11 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
83
82
  return nextSeen;
84
83
  });
85
84
  }, [storageAdapter]);
86
- const registerTarget = (0, react_1.useCallback)((id, measurement) => {
85
+ const registerTarget = (0, react_1.useCallback)((id, measurement, ref) => {
87
86
  setTargets(prev => ({ ...prev, [id]: measurement }));
87
+ if (ref) {
88
+ targetRefs.current[id] = ref;
89
+ }
88
90
  }, []);
89
91
  const unregisterTarget = (0, react_1.useCallback)((id) => {
90
92
  setTargets(prev => {
@@ -92,6 +94,37 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
92
94
  delete next[id];
93
95
  return next;
94
96
  });
97
+ delete targetRefs.current[id];
98
+ }, []);
99
+ const _registerScrollRef = (0, react_1.useCallback)((ref) => {
100
+ scrollRef.current = ref;
101
+ }, []);
102
+ /**
103
+ * Calculates the relative position of a target to the registered ScrollView
104
+ * and triggers a native scrollTo call.
105
+ */
106
+ const requestScroll = (0, react_1.useCallback)((targetId) => {
107
+ const target = targetRefs.current[targetId];
108
+ const scroll = scrollRef.current;
109
+ if (!target || !scroll)
110
+ return;
111
+ // Use measureLayout for scrollable positioning calculation
112
+ const targetHandle = (0, react_native_1.findNodeHandle)(target);
113
+ const scrollHandle = (0, react_native_1.findNodeHandle)(scroll);
114
+ if (targetHandle && scrollHandle) {
115
+ try {
116
+ target.measureLayout(scrollHandle, (_x, y) => {
117
+ // Scroll target into view with some top padding
118
+ scroll.scrollTo({ y: Math.max(0, y - 100), animated: true });
119
+ }, () => {
120
+ console.warn(`[rn-dap] Failed to measure layout for target: ${targetId}`);
121
+ });
122
+ }
123
+ catch (e) {
124
+ // Fallback if measureLayout isn't available on the component ref directly
125
+ console.warn('[rn-dap] measureLayout failed', e);
126
+ }
127
+ }
95
128
  }, []);
96
129
  const startTour = (0, react_1.useCallback)((tourId) => {
97
130
  if (tours[tourId] && !seenTours[tourId]) {
@@ -102,11 +135,13 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
102
135
  console.warn(`[rn-dap] Tour with id ${tourId} not found.`);
103
136
  }
104
137
  }, [tours, seenTours]);
105
- // Uses activeTourIdRef to avoid stale closures and prevent cascading re-renders.
106
138
  const stopTour = (0, react_1.useCallback)((markAsSeen = true) => {
107
139
  const currentTourId = activeTourIdRef.current;
108
- if (currentTourId && markAsSeen) {
109
- saveSeenTour(currentTourId);
140
+ if (currentTourId) {
141
+ if (markAsSeen) {
142
+ saveSeenTour(currentTourId);
143
+ }
144
+ tourIdJustStoppedRef.current = currentTourId;
110
145
  }
111
146
  setActiveTourId(null);
112
147
  setCurrentStepIndex(0);
@@ -118,7 +153,6 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
118
153
  setCurrentStepIndex(prev => prev + 1);
119
154
  }
120
155
  else {
121
- // Finished the last step
122
156
  stopTour(true);
123
157
  }
124
158
  }
@@ -128,29 +162,28 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
128
162
  setCurrentStepIndex(prev => prev - 1);
129
163
  }
130
164
  }, [currentStepIndex]);
131
- /** Timer ref for debounced auto-start. */
132
165
  const autoStartTimerRef = (0, react_1.useRef)();
133
- // Auto-Start Engine — debounced so multi-pass measurements can settle.
134
166
  (0, react_1.useEffect)(() => {
135
- // Wait until storage is loaded, and ensure no tour is currently running
136
167
  if (!isStorageLoaded || activeTourId)
137
168
  return;
138
- // Clear any previously scheduled auto-start
139
169
  if (autoStartTimerRef.current) {
140
170
  clearTimeout(autoStartTimerRef.current);
141
171
  }
142
172
  autoStartTimerRef.current = setTimeout(() => {
143
173
  for (const tourId of Object.keys(tours)) {
144
174
  const tour = tours[tourId];
145
- if (tour.autoStart && !seenTours[tourId] && tour.steps.length > 0) {
175
+ const wasJustStopped = tourIdJustStoppedRef.current === tourId;
176
+ if (tour.autoStart && !seenTours[tourId] && !wasJustStopped && tour.steps.length > 0) {
146
177
  const firstTargetId = tour.steps[0].targetId;
147
- // If the first target of an unread, auto-starting tour is mounted
148
178
  if (targets[firstTargetId]) {
149
179
  startTour(tourId);
150
- break; // Start only one auto-tour at a time
180
+ break;
151
181
  }
152
182
  }
153
183
  }
184
+ if (tourIdJustStoppedRef.current && seenTours[tourIdJustStoppedRef.current]) {
185
+ tourIdJustStoppedRef.current = null;
186
+ }
154
187
  }, AUTO_START_DEBOUNCE_MS);
155
188
  return () => {
156
189
  if (autoStartTimerRef.current) {
@@ -169,7 +202,9 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
169
202
  activeTour,
170
203
  currentStepIndex,
171
204
  targets,
172
- seenTours
205
+ seenTours,
206
+ requestScroll,
207
+ _registerScrollRef
173
208
  }), [
174
209
  registerTarget,
175
210
  unregisterTarget,
@@ -180,7 +215,9 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
180
215
  activeTour,
181
216
  currentStepIndex,
182
217
  targets,
183
- seenTours
218
+ seenTours,
219
+ requestScroll,
220
+ _registerScrollRef
184
221
  ]);
185
222
  return (<DapContext_1.DapContext.Provider value={contextValue}>
186
223
  {children}
@@ -0,0 +1,8 @@
1
+ import React from 'react';
2
+ import { ScrollViewProps } from 'react-native';
3
+ /**
4
+ * A wrapper for the React Native ScrollView that integrates with the rn-smart-tour
5
+ * Auto-Scroll engine. When used, the library can automatically scroll to bring
6
+ * tour targets into the user's view.
7
+ */
8
+ export declare const DapScrollView: React.FC<ScrollViewProps>;
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DapScrollView = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const DapContext_1 = require("./DapContext");
40
+ /**
41
+ * A wrapper for the React Native ScrollView that integrates with the rn-smart-tour
42
+ * Auto-Scroll engine. When used, the library can automatically scroll to bring
43
+ * tour targets into the user's view.
44
+ */
45
+ const DapScrollView = (props) => {
46
+ const context = (0, react_1.useContext)(DapContext_1.DapContext);
47
+ const scrollRef = (0, react_1.useRef)(null);
48
+ (0, react_1.useEffect)(() => {
49
+ if (context?._registerScrollRef && scrollRef.current) {
50
+ context._registerScrollRef(scrollRef.current);
51
+ }
52
+ return () => {
53
+ if (context?._registerScrollRef) {
54
+ context._registerScrollRef(null);
55
+ }
56
+ };
57
+ }, [context]);
58
+ return (<react_native_1.ScrollView ref={scrollRef} {...props}>
59
+ {props.children}
60
+ </react_native_1.ScrollView>);
61
+ };
62
+ exports.DapScrollView = DapScrollView;
@@ -2,7 +2,7 @@ import React, { ReactElement } from 'react';
2
2
  import { ViewProps } from 'react-native';
3
3
  interface DapTargetProps extends ViewProps {
4
4
  name: string;
5
- children: ReactElement;
5
+ children: ReactElement | ReactElement[];
6
6
  }
7
7
  export declare const DapTarget: React.FC<DapTargetProps>;
8
8
  export {};
package/dist/DapTarget.js CHANGED
@@ -85,7 +85,8 @@ const DapTarget = ({ name, children, ...props }) => {
85
85
  const next = { x, y, width, height };
86
86
  if (hasPositionChanged(lastMeasurementRef.current, next)) {
87
87
  lastMeasurementRef.current = next;
88
- context.registerTarget(name, next);
88
+ // Pass viewRef.current to enable Auto-Scroll layout measurements
89
+ context.registerTarget(name, next, viewRef.current);
89
90
  }
90
91
  });
91
92
  }, delay);
@@ -116,7 +117,19 @@ const DapTarget = ({ name, children, ...props }) => {
116
117
  unregisterTarget?.(name);
117
118
  };
118
119
  }, [name, unregisterTarget, clearAllTimers]);
119
- // collapsable={false} is vital for Android, otherwise it gets optimized away and measure fails
120
+ /**
121
+ * Auto-Scroll Trigger: If this target becomes the active step in a tour,
122
+ * request the provider to bring it into view.
123
+ */
124
+ (0, react_1.useEffect)(() => {
125
+ if (!context || !context.activeTour)
126
+ return;
127
+ const activeStep = context.activeTour.steps[context.currentStepIndex];
128
+ if (activeStep?.targetId === name && context.requestScroll) {
129
+ context.requestScroll(name);
130
+ }
131
+ }, [context?.activeTour, context?.currentStepIndex, context?.requestScroll, name]);
132
+ // Always use standard View wrapper (asChild reverted per user preference)
120
133
  return (<react_native_1.View ref={viewRef} onLayout={handleLayout} collapsable={false} {...props}>
121
134
  {children}
122
135
  </react_native_1.View>);
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './DapProvider';
2
2
  export * from './DapTarget';
3
+ export * from './DapOverlay';
4
+ export * from './DapScrollView';
3
5
  export * from './useDap';
4
6
  export * from './types';
package/dist/index.js CHANGED
@@ -16,5 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./DapProvider"), exports);
18
18
  __exportStar(require("./DapTarget"), exports);
19
+ __exportStar(require("./DapOverlay"), exports);
20
+ __exportStar(require("./DapScrollView"), exports);
19
21
  __exportStar(require("./useDap"), exports);
20
22
  __exportStar(require("./types"), exports);
package/dist/types.d.ts CHANGED
@@ -20,7 +20,7 @@ export interface StorageAdapter {
20
20
  setItem: (key: string, value: string) => Promise<void> | void;
21
21
  }
22
22
  export interface DapContextType {
23
- registerTarget: (id: string, measurement: TargetMeasurement) => void;
23
+ registerTarget: (id: string, measurement: TargetMeasurement, ref?: any) => void;
24
24
  unregisterTarget: (id: string) => void;
25
25
  startTour: (tourId: string) => void;
26
26
  nextStep: () => void;
@@ -30,4 +30,8 @@ export interface DapContextType {
30
30
  currentStepIndex: number;
31
31
  targets: Record<string, TargetMeasurement>;
32
32
  seenTours: Record<string, boolean>;
33
+ /** Requests the provider to scroll the active DapScrollView to a specific target. */
34
+ requestScroll: (targetId: string) => void;
35
+ /** Internal use only: registers a ScrollView's native ref for coordinate calculations. */
36
+ _registerScrollRef: (ref: any) => void;
33
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-smart-tour",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "Enterprise-grade Digital Adoption Platform (DAP) package for React Native. Provides guided walkthroughs, tooltips, and app tours.",
5
5
  "author": "Vishwas Gaur",
6
6
  "license": "MIT",
@@ -9,14 +9,6 @@
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
- "repository": {
13
- "type": "git",
14
- "url": "https://github.com/Vishwasgaur0819/rn-smart-tour"
15
- },
16
- "homepage": "https://github.com/Vishwasgaur0819/rn-smart-tour#readme",
17
- "bugs": {
18
- "url": "https://github.com/Vishwasgaur0819/rn-smart-tour/issues"
19
- },
20
12
  "keywords": [
21
13
  "react-native",
22
14
  "tour",