rn-smart-tour 1.0.3 → 1.0.5

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
@@ -1,15 +1,23 @@
1
1
  # rn-smart-tour
2
2
 
3
- An enterprise-grade Digital Adoption Platform (DAP) package for React Native. Easily add product tours, guided walkthroughs, and onboarding overlays directly into your app without intrusive code changes.
3
+ <p align="center">
4
+ <b>Enterprise-grade Digital Adoption Platform (DAP) for React Native</b><br>
5
+ Easily add product tours, guided walkthroughs, and onboarding overlays without intrusive code changes.
6
+ </p>
4
7
 
5
- ## Features
6
- - **Multi-Pass Measurement**: Wraps your components and uses the native `measureInWindow` API with a self-correcting, multi-pass strategy — so highlights land correctly even during screen-transition animations.
7
- - **Rotation & Resize Aware**: Automatically re-measures targets when screen dimensions change (rotation, split-screen, foldables).
8
- - **Debounced Auto-Start Engine**: Automatically triggers tours when target elements mount, with a built-in debounce to ensure the overlay uses fully settled coordinates.
9
- - **Seen State Caching**: Connect any local storage database to ensure users only see tours once.
10
- - **Smart Overlays**: Creates highlighted holes in backdrops over completely custom UIs, with Back/Next/Skip navigation.
8
+ ---
9
+
10
+ ## Features
11
+
12
+ - 🎯 **Multi-Pass Measurement**: Native `measureInWindow` API with self-correcting strategy for animation resilience.
13
+ - 📱 **Rotation & Resize Aware**: Targets re-measure automatically on orientation change or split-screen.
14
+ - ⚡ **Auto-Start Engine**: Trigger tours instantly on mount with a smart debounce for layout stability.
15
+ - 💾 **Seen State Caching**: Persistent "only-once" logic with pluggable storage (AsyncStorage, MMKV, etc.).
16
+ - 🎨 **Smart Overlays**: Dynamic cutouts with Back/Next/Skip navigation and step indicators.
11
17
 
12
- ## Installation
18
+ ---
19
+
20
+ ## 📦 Installation
13
21
 
14
22
  ```sh
15
23
  npm install rn-smart-tour
@@ -17,202 +25,126 @@ npm install rn-smart-tour
17
25
 
18
26
  ---
19
27
 
20
- ## 🚀 Quick Start Guide
21
-
22
- Adding a product tour to your app only takes 3 simple steps:
28
+ ## 🚀 Quick Start
23
29
 
24
- ### Step 1: Wrap your App
25
- At the very top of your application (usually `App.tsx`), wrap everything in the `<DapProvider>`. Here you define the tours you want to show your users.
30
+ ### 1. Wrap your App
31
+ Wrap your root component in the `DapProvider` and define your tours.
26
32
 
27
33
  ```tsx
28
- import React from 'react';
29
34
  import { DapProvider } from 'rn-smart-tour';
30
35
 
31
- // Define the steps of your tour here!
32
- const MY_TOURS = {
36
+ const TOURS = {
33
37
  'welcome-tour': {
34
38
  id: 'welcome-tour',
35
- steps: [
36
- {
37
- targetId: 'my-first-button',
38
- title: 'Welcome to the App!',
39
- description: 'Tapping this button saves your progress.',
40
- }
41
- ]
39
+ steps: [{
40
+ targetId: 'save-btn',
41
+ title: 'Welcome!',
42
+ description: 'Tap here to save your progress.',
43
+ }]
42
44
  }
43
45
  };
44
46
 
45
47
  export default function App() {
46
48
  return (
47
- <DapProvider tours={MY_TOURS}>
48
- <MainScreen />
49
+ <DapProvider tours={TOURS}>
50
+ <MainApp />
49
51
  </DapProvider>
50
52
  );
51
53
  }
52
54
  ```
53
55
 
54
- ### Step 2: Target an Element
55
- Go to any screen in your app and decide what button or view you want to highlight. Wrap that element with `<DapTarget>`. Make sure the `name` matches the `targetId` from Step 1!
56
+ ### 2. Mark your Target
57
+ Wrap any view or button you want to highlight with `DapTarget`.
56
58
 
57
59
  ```tsx
58
- import { View, Button } from 'react-native';
59
60
  import { DapTarget } from 'rn-smart-tour';
60
61
 
61
- export const MainScreen = () => {
62
- return (
63
- <View style={{ marginTop: 100 }}>
64
- {/* Wrap the button you want to highlight! */}
65
- <DapTarget name="my-first-button">
66
- <Button title="Save Button" onPress={() => {}} />
67
- </DapTarget>
68
- </View>
69
- );
70
- };
62
+ const MyButton = () => (
63
+ <DapTarget name="save-btn">
64
+ <Button title="Save" onPress={...} />
65
+ </DapTarget>
66
+ );
71
67
  ```
72
68
 
73
- ### Step 3: Trigger the Tour!
74
- To start the walkthrough, just call `startTour` anywhere inside your app.
69
+ ### 3. Start the Tour
70
+ Use the `useDap` hook to trigger the onboarding.
75
71
 
76
72
  ```tsx
77
- import { Button } from 'react-native';
78
73
  import { useDap } from 'rn-smart-tour';
79
74
 
80
- export const HelpMenu = () => {
81
- const { startTour } = useDap();
82
-
83
- return (
84
- <Button
85
- title="Start Walkthrough"
86
- onPress={() => startTour('welcome-tour')}
87
- />
88
- );
89
- }
75
+ const { startTour } = useDap();
76
+ // ...
77
+ <Button title="Help" onPress={() => startTour('welcome-tour')} />
90
78
  ```
91
79
 
92
80
  ---
93
81
 
94
- ## 🧠 Advanced Usage (Pro Features)
95
-
96
- ### 1. Auto-Start & Show "Only Once"
97
- For true enterprise onboarding, you want the tour to start *automatically* when a user visits a new screen, and never show it to them again after they finish it.
98
-
99
- By passing a `storageAdapter` (like `AsyncStorage`) into the Provider, the package will permanently remember who has seen the tour!
100
-
101
- ```tsx
102
- import AsyncStorage from '@react-native-async-storage/async-storage';
103
-
104
- const MY_TOURS = {
105
- 'welcome-tour': {
106
- id: 'welcome-tour',
107
- autoStart: true, // Automatically starts when "my-first-button" mounts!
108
- steps: [ ... ]
109
- }
110
- };
111
-
112
- // Map the Storage commands
113
- const myStorage = {
114
- getItem: async (key) => await AsyncStorage.getItem(key),
115
- setItem: async (key, value) => { await AsyncStorage.setItem(key, value); }
116
- };
117
-
118
- // Pass it to the Provider
119
- <DapProvider tours={MY_TOURS} storageAdapter={myStorage}>
120
- ```
82
+ ## 🧠 Technical Architecture
121
83
 
122
- ### 2. How Measurement Works
84
+ <details>
85
+ <summary><b>View How Measurements Work (Click to expand)</b></summary>
123
86
 
124
- When a `<DapTarget>` mounts or re-layouts, it does **not** rely on a single measurement. Instead, it uses a **multi-pass strategy** to guarantee accuracy:
87
+ To guarantee accuracy during navigation animations, `rn-smart-tour` uses a **multi-pass strategy**:
125
88
 
126
- | Pass | Delay | Purpose |
127
- |------|-------|---------|
128
- | 1st | 100ms | Fast first estimate may capture mid-animation coordinates |
129
- | 2nd | 500ms | Self-corrects after most navigation transitions finish |
130
- | 3rd | 1000ms | Final safety net for slow animations or async layout shifts |
89
+ - **Pass 1 (100ms)**: Fast first estimate.
90
+ - **Pass 2 (500ms)**: Corrects after most screen transitions finish.
91
+ - **Pass 3 (1000ms)**: Final safety net for slow async layout shifts.
131
92
 
132
- - Each new `onLayout` event **cancels** all pending timers and schedules fresh measurements.
133
- - A measurement is only sent to the Provider if the position has **actually changed** (>1pt threshold), avoiding unnecessary re-renders.
134
- - On **rotation/resize**, all targets automatically re-measure themselves.
135
- - On unmount, all timers are cleaned up and the target is unregistered.
93
+ Measurements only trigger a re-render if the position changes by more than **1pt (threshold)**.
136
94
 
137
- #### Auto-Start Debounce
95
+ </details>
138
96
 
139
- When `autoStart: true` is set on a tour, the overlay does **not** appear the instant a target registers. Instead, it waits **300ms** before starting. If a more accurate measurement arrives during that window (e.g., the 500ms pass corrects the 100ms pass), the debounce resets and the overlay uses the final, settled coordinates.
97
+ <details>
98
+ <summary><b>Auto-Start & Debounce Settings</b></summary>
140
99
 
141
- This is why you may notice a brief (~300ms) delay before an auto-start tour appears it's by design to prevent misaligned highlights.
100
+ When `autoStart: true` is enabled, the overlay waits **300ms** after registration. This allows the multi-pass system to settle on the final coordinates before the hole is cut into the backdrop.
142
101
 
143
- #### Tuning Constants
102
+ </details>
144
103
 
145
- If you need to adjust timing for your app's specific animation durations, the following constants can be modified in the source:
146
-
147
- | Constant | File | Default | Description |
148
- |----------|------|---------|-------------|
149
- | `MEASUREMENT_DELAYS` | `DapTarget.tsx` | `[100, 500, 1000]` | Multi-pass measurement intervals (ms) |
150
- | `POSITION_THRESHOLD` | `DapTarget.tsx` | `1` | Minimum position change (points) to trigger re-registration |
151
- | `AUTO_START_DEBOUNCE_MS` | `DapProvider.tsx` | `300` | Debounce delay (ms) before auto-starting a tour |
152
-
153
- ### 3. Programmatic Control
154
-
155
- The `useDap()` hook exposes full control over tours:
156
-
157
- ```tsx
158
- const { startTour, stopTour, nextStep, prevStep, activeTour, currentStepIndex } = useDap();
159
-
160
- // Start a specific tour
161
- startTour('welcome-tour');
162
-
163
- // Stop and mark as seen (default)
164
- stopTour();
165
-
166
- // Stop WITHOUT marking as seen (user can see it again)
167
- stopTour(false);
168
-
169
- // Navigate between steps
170
- nextStep();
171
- prevStep();
172
- ```
104
+ ---
173
105
 
174
- ## API Reference
106
+ ## 🛠 API Reference
175
107
 
176
- ### Tour Object
108
+ ### Tour Configuration
177
109
  | Property | Type | Description |
178
- |-----------|------|-------------|
179
- | `id` | `string` | Unique identifier. Needed for cache tracking. |
180
- | `autoStart` | `boolean` | If true, automatically renders when the first step's target mounts on the screen. |
181
- | `steps` | `TourStep[]` | The sequence of highlighted elements and tooltips. |
110
+ |:---|:---|:---|
111
+ | `id` | `string` | Unique identifier for caching. |
112
+ | `autoStart` | `boolean` | Trigger as soon as the first target mounts. |
113
+ | `steps` | `TourStep[]` | Sequence of highlight steps. |
182
114
 
183
- ### TourStep Object
184
- | Property | Type | Default | Description |
185
- |-----------|------|---------|-------------|
186
- | `targetId` | `string` | — | Must directly match the `name=""` prop passed to `<DapTarget>`. |
187
- | `title` | `string` | — | Large text inside the tooltip. |
188
- | `description` | `string` | — | Context explanation inside the tooltip. |
189
- | `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `'bottom'` | Where the tooltip appears relative to the highlighted target. |
190
-
191
- ### DapTarget Props
115
+ ### DapTarget
192
116
  | Property | Type | Description |
193
- |-----------|------|-------------|
117
+ |:---|:---|:---|
194
118
  | `name` | `string` | Unique identifier that matches a `targetId` in a tour step. |
195
119
  | `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). |
196
121
  | `...props` | `ViewProps` | All standard React Native `View` props are forwarded. |
197
122
 
198
- ### useDap() Hook
199
- | Property | Type | Description |
200
- |-----------|------|-------------|
201
- | `startTour(id)` | `(tourId: string) => void` | Start a tour by its ID. |
202
- | `stopTour(markAsSeen?)` | `(markAsSeen?: boolean) => void` | Stop the active tour. Pass `false` to allow the tour to show again. |
203
- | `nextStep()` | `() => void` | Advance to the next step. Finishes the tour if on the last step. |
204
- | `prevStep()` | `() => void` | Go back to the previous step. |
205
- | `activeTour` | `Tour \| null` | The currently active tour object, or `null`. |
206
- | `currentStepIndex` | `number` | Index of the current step in the active tour. |
207
- | `targets` | `Record<string, TargetMeasurement>` | All registered target measurements. |
208
- | `seenTours` | `Record<string, boolean>` | Map of tour IDs to whether they've been seen. |
209
-
210
- ### StorageAdapter Interface
211
- | Method | Type | Description |
212
- |--------|------|-------------|
213
- | `getItem` | `(key: string) => Promise<string \| null> \| string \| null` | Retrieve a stored value by key. |
214
- | `setItem` | `(key: string, value: string) => Promise<void> \| void` | Store a value by key. |
215
-
216
- Compatible with `AsyncStorage`, `MMKV`, or any key-value store that implements this interface.
217
- # rn-smart-tour
218
-
123
+ ### TourStep
124
+ | Property | Type | Default | Description |
125
+ |:---|:---|:---|:---|
126
+ | `targetId` | `string` | | Matches the `name` prop in `<DapTarget>`. |
127
+ | `title` | `string` | | Tooltip header. |
128
+ | `description` | `string` | | Tooltip body. |
129
+ | `position` | `string` | `'bottom'` | `top`, `bottom`, `left`, `right`. |
130
+
131
+ ### `useDap()` Hook
132
+ | Method | Description |
133
+ |:---|:---|
134
+ | `startTour(id)` | Start a tour by ID. |
135
+ | `stopTour(markAsSeen?)`| End tour. Pass `false` to keep it unread. |
136
+ | `nextStep()` / `prevStep()` | Manual step navigation. |
137
+ | `activeTour` | Current active tour object. |
138
+ | `currentStepIndex` | Current step number (0-indexed). |
139
+
140
+ ---
141
+
142
+ ## 🤝 Contributing
143
+
144
+ Contributions are welcome! Please feel free to submit a Pull Request.
145
+
146
+ ---
147
+
148
+ <p align="center">
149
+ <b>rn-smart-tour</b> • Built with ❤️ for the React Native community.
150
+ </p>
@@ -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>
@@ -50,9 +50,15 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
50
50
  const [currentStepIndex, setCurrentStepIndex] = (0, react_1.useState)(0);
51
51
  const [seenTours, setSeenTours] = (0, react_1.useState)({});
52
52
  const [isStorageLoaded, setIsStorageLoaded] = (0, react_1.useState)(!storageAdapter);
53
- // Ref to avoid stale closures — always holds the latest activeTourId.
53
+ // Ref to always hold the latest activeTourId, avoiding stale closures.
54
54
  const activeTourIdRef = (0, react_1.useRef)(activeTourId);
55
55
  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
+ const tourIdJustStoppedRef = (0, react_1.useRef)(null);
56
62
  // Load seen tours on mount if a storage adapter is provided
57
63
  (0, react_1.useEffect)(() => {
58
64
  const loadStorage = async () => {
@@ -102,11 +108,14 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
102
108
  console.warn(`[rn-dap] Tour with id ${tourId} not found.`);
103
109
  }
104
110
  }, [tours, seenTours]);
105
- // Uses activeTourIdRef to avoid stale closures and prevent cascading re-renders.
106
111
  const stopTour = (0, react_1.useCallback)((markAsSeen = true) => {
107
112
  const currentTourId = activeTourIdRef.current;
108
- if (currentTourId && markAsSeen) {
109
- saveSeenTour(currentTourId);
113
+ if (currentTourId) {
114
+ if (markAsSeen) {
115
+ saveSeenTour(currentTourId);
116
+ }
117
+ // Block this specific tour from auto-restarting until coordinates or seenTours settle.
118
+ tourIdJustStoppedRef.current = currentTourId;
110
119
  }
111
120
  setActiveTourId(null);
112
121
  setCurrentStepIndex(0);
@@ -142,7 +151,9 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
142
151
  autoStartTimerRef.current = setTimeout(() => {
143
152
  for (const tourId of Object.keys(tours)) {
144
153
  const tour = tours[tourId];
145
- if (tour.autoStart && !seenTours[tourId] && tour.steps.length > 0) {
154
+ // Skip if this tour was just manually stopped and haven't confirmed 'seen' yet.
155
+ const wasJustStopped = tourIdJustStoppedRef.current === tourId;
156
+ if (tour.autoStart && !seenTours[tourId] && !wasJustStopped && tour.steps.length > 0) {
146
157
  const firstTargetId = tour.steps[0].targetId;
147
158
  // If the first target of an unread, auto-starting tour is mounted
148
159
  if (targets[firstTargetId]) {
@@ -151,6 +162,10 @@ const DapProvider = ({ children, tours, storageAdapter }) => {
151
162
  }
152
163
  }
153
164
  }
165
+ // Cleanup the "just stopped" ref once the seenTours state finally reflects the closure.
166
+ if (tourIdJustStoppedRef.current && seenTours[tourIdJustStoppedRef.current]) {
167
+ tourIdJustStoppedRef.current = null;
168
+ }
154
169
  }, AUTO_START_DEBOUNCE_MS);
155
170
  return () => {
156
171
  if (autoStartTimerRef.current) {
@@ -3,6 +3,12 @@ import { ViewProps } from 'react-native';
3
3
  interface DapTargetProps extends ViewProps {
4
4
  name: string;
5
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;
6
12
  }
7
13
  export declare const DapTarget: React.FC<DapTargetProps>;
8
14
  export {};
package/dist/DapTarget.js CHANGED
@@ -45,7 +45,22 @@ 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
- const DapTarget = ({ name, children, ...props }) => {
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 }) => {
49
64
  const viewRef = (0, react_1.useRef)(null);
50
65
  const context = (0, react_1.useContext)(DapContext_1.DapContext);
51
66
  /** Holds all scheduled timer IDs so we can cancel them on re-layout or unmount. */
@@ -116,7 +131,26 @@ const DapTarget = ({ name, children, ...props }) => {
116
131
  unregisterTarget?.(name);
117
132
  };
118
133
  }, [name, unregisterTarget, clearAllTimers]);
119
- // collapsable={false} is vital for Android, otherwise it gets optimized away and measure fails
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.');
151
+ }
152
+ }
153
+ // Fallback to standard View wrapper
120
154
  return (<react_native_1.View ref={viewRef} onLayout={handleLayout} collapsable={false} {...props}>
121
155
  {children}
122
156
  </react_native_1.View>);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-smart-tour",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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",