rn-smart-tour 1.0.5 → 1.0.8

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,12 +127,18 @@ 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
+
115
137
  ### DapTarget
116
138
  | Property | Type | Description |
117
139
  |:---|:---|:---|
118
140
  | `name` | `string` | Unique identifier that matches a `targetId` in a tour step. |
119
141
  | `children` | `ReactElement` | The UI element to wrap and highlight. |
120
- | `asChild` | `boolean` | **New!** If true, clones the child to avoid an extra View wrapper. (Crucial for flex/percentage layouts). |
121
142
  | `...props` | `ViewProps` | All standard React Native `View` props are forwarded. |
122
143
 
123
144
  ### TourStep
@@ -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,15 +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 always hold the latest activeTourId, avoiding stale closures.
54
49
  const activeTourIdRef = (0, react_1.useRef)(activeTourId);
55
50
  activeTourIdRef.current = activeTourId;
56
- /**
57
- * Ref to track the ID of a tour that was just manually stopped/skipped.
58
- * This prevents the auto-start engine from immediately restarting the same tour
59
- * before the 'seenTours' state update has been processed.
60
- */
61
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);
62
56
  // Load seen tours on mount if a storage adapter is provided
63
57
  (0, react_1.useEffect)(() => {
64
58
  const loadStorage = async () => {
@@ -81,7 +75,6 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
81
75
  setSeenTours(prev => {
82
76
  const nextSeen = { ...prev, [tourId]: true };
83
77
  if (storageAdapter) {
84
- // Safe side-effect here for async storage, as long as it's fire-and-forget
85
78
  Promise.resolve(storageAdapter.setItem(STORAGE_KEY, JSON.stringify(nextSeen))).catch(e => {
86
79
  console.error('[rn-dap] failed to save storage', e);
87
80
  });
@@ -89,8 +82,11 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
89
82
  return nextSeen;
90
83
  });
91
84
  }, [storageAdapter]);
92
- const registerTarget = (0, react_1.useCallback)((id, measurement) => {
85
+ const registerTarget = (0, react_1.useCallback)((id, measurement, ref) => {
93
86
  setTargets(prev => ({ ...prev, [id]: measurement }));
87
+ if (ref) {
88
+ targetRefs.current[id] = ref;
89
+ }
94
90
  }, []);
95
91
  const unregisterTarget = (0, react_1.useCallback)((id) => {
96
92
  setTargets(prev => {
@@ -98,6 +94,37 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
98
94
  delete next[id];
99
95
  return next;
100
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
+ }
101
128
  }, []);
102
129
  const startTour = (0, react_1.useCallback)((tourId) => {
103
130
  if (tours[tourId] && !seenTours[tourId]) {
@@ -114,7 +141,6 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
114
141
  if (markAsSeen) {
115
142
  saveSeenTour(currentTourId);
116
143
  }
117
- // Block this specific tour from auto-restarting until coordinates or seenTours settle.
118
144
  tourIdJustStoppedRef.current = currentTourId;
119
145
  }
120
146
  setActiveTourId(null);
@@ -127,7 +153,6 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
127
153
  setCurrentStepIndex(prev => prev + 1);
128
154
  }
129
155
  else {
130
- // Finished the last step
131
156
  stopTour(true);
132
157
  }
133
158
  }
@@ -137,32 +162,25 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
137
162
  setCurrentStepIndex(prev => prev - 1);
138
163
  }
139
164
  }, [currentStepIndex]);
140
- /** Timer ref for debounced auto-start. */
141
165
  const autoStartTimerRef = (0, react_1.useRef)();
142
- // Auto-Start Engine — debounced so multi-pass measurements can settle.
143
166
  (0, react_1.useEffect)(() => {
144
- // Wait until storage is loaded, and ensure no tour is currently running
145
167
  if (!isStorageLoaded || activeTourId)
146
168
  return;
147
- // Clear any previously scheduled auto-start
148
169
  if (autoStartTimerRef.current) {
149
170
  clearTimeout(autoStartTimerRef.current);
150
171
  }
151
172
  autoStartTimerRef.current = setTimeout(() => {
152
173
  for (const tourId of Object.keys(tours)) {
153
174
  const tour = tours[tourId];
154
- // Skip if this tour was just manually stopped and haven't confirmed 'seen' yet.
155
175
  const wasJustStopped = tourIdJustStoppedRef.current === tourId;
156
176
  if (tour.autoStart && !seenTours[tourId] && !wasJustStopped && tour.steps.length > 0) {
157
177
  const firstTargetId = tour.steps[0].targetId;
158
- // If the first target of an unread, auto-starting tour is mounted
159
178
  if (targets[firstTargetId]) {
160
179
  startTour(tourId);
161
- break; // Start only one auto-tour at a time
180
+ break;
162
181
  }
163
182
  }
164
183
  }
165
- // Cleanup the "just stopped" ref once the seenTours state finally reflects the closure.
166
184
  if (tourIdJustStoppedRef.current && seenTours[tourIdJustStoppedRef.current]) {
167
185
  tourIdJustStoppedRef.current = null;
168
186
  }
@@ -184,7 +202,9 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
184
202
  activeTour,
185
203
  currentStepIndex,
186
204
  targets,
187
- seenTours
205
+ seenTours,
206
+ requestScroll,
207
+ _registerScrollRef
188
208
  }), [
189
209
  registerTarget,
190
210
  unregisterTarget,
@@ -195,7 +215,9 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
195
215
  activeTour,
196
216
  currentStepIndex,
197
217
  targets,
198
- seenTours
218
+ seenTours,
219
+ requestScroll,
220
+ _registerScrollRef
199
221
  ]);
200
222
  return (<DapContext_1.DapContext.Provider value={contextValue}>
201
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,13 +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;
6
- /**
7
- * If true, DapTarget will not wrap your child in a View. Instead, it will
8
- * clone the child and inject the ref/onLayout logic directly.
9
- * Useful for maintaining flex layouts or percentage widths (e.g. 33%).
10
- */
11
- asChild?: boolean;
5
+ children: ReactElement | ReactElement[];
12
6
  }
13
7
  export declare const DapTarget: React.FC<DapTargetProps>;
14
8
  export {};
package/dist/DapTarget.js CHANGED
@@ -45,22 +45,7 @@ const DapContext_1 = require("./DapContext");
45
45
  const MEASUREMENT_DELAYS = [100, 500, 1000];
46
46
  /** Threshold in points — ignore sub-pixel drift to avoid unnecessary re-registers. */
47
47
  const POSITION_THRESHOLD = 1;
48
- /**
49
- * Utility to merge multiple refs into a single callback ref.
50
- */
51
- function setRefs(...refs) {
52
- return (value) => {
53
- refs.forEach((ref) => {
54
- if (typeof ref === 'function') {
55
- ref(value);
56
- }
57
- else if (ref && typeof ref === 'object') {
58
- ref.current = value;
59
- }
60
- });
61
- };
62
- }
63
- const DapTarget = ({ name, children, asChild, ...props }) => {
48
+ const DapTarget = ({ name, children, ...props }) => {
64
49
  const viewRef = (0, react_1.useRef)(null);
65
50
  const context = (0, react_1.useContext)(DapContext_1.DapContext);
66
51
  /** Holds all scheduled timer IDs so we can cancel them on re-layout or unmount. */
@@ -100,7 +85,8 @@ const DapTarget = ({ name, children, asChild, ...props }) => {
100
85
  const next = { x, y, width, height };
101
86
  if (hasPositionChanged(lastMeasurementRef.current, next)) {
102
87
  lastMeasurementRef.current = next;
103
- context.registerTarget(name, next);
88
+ // Pass viewRef.current to enable Auto-Scroll layout measurements
89
+ context.registerTarget(name, next, viewRef.current);
104
90
  }
105
91
  });
106
92
  }, delay);
@@ -131,26 +117,19 @@ const DapTarget = ({ name, children, asChild, ...props }) => {
131
117
  unregisterTarget?.(name);
132
118
  };
133
119
  }, [name, unregisterTarget, clearAllTimers]);
134
- // If asChild is enabled, clone the child and inject measurement logic.
135
- if (asChild && (0, react_1.isValidElement)(children)) {
136
- try {
137
- const child = react_1.Children.only(children);
138
- return (0, react_1.cloneElement)(child, {
139
- ...props,
140
- ref: setRefs(viewRef, child.ref),
141
- onLayout: (e) => {
142
- handleLayout(e);
143
- child.props.onLayout?.(e);
144
- },
145
- // collapsable={false} is vital for Android measurement
146
- collapsable: false,
147
- });
148
- }
149
- catch (e) {
150
- console.warn('[rn-dap] asChild requires a single React element as a child.');
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);
151
130
  }
152
- }
153
- // Fallback to standard View wrapper
131
+ }, [context?.activeTour, context?.currentStepIndex, context?.requestScroll, name]);
132
+ // Always use standard View wrapper (asChild reverted per user preference)
154
133
  return (<react_native_1.View ref={viewRef} onLayout={handleLayout} collapsable={false} {...props}>
155
134
  {children}
156
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,9 +1,17 @@
1
1
  {
2
2
  "name": "rn-smart-tour",
3
- "version": "1.0.5",
3
+ "version": "1.0.8",
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",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/Vishwasgaur0819/rn-smart-tour.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Vishwasgaur0819/rn-smart-tour/issues"
13
+ },
14
+ "homepage": "https://github.com/Vishwasgaur0819/rn-smart-tour#readme",
7
15
  "main": "dist/index.js",
8
16
  "types": "dist/index.d.ts",
9
17
  "files": [
@@ -36,4 +44,4 @@
36
44
  "react-native": "0.73.11",
37
45
  "typescript": "^5.9.3"
38
46
  }
39
- }
47
+ }