Skip to content

Commit 2c9c664

Browse files
committed
feat: implement basic functionality
1 parent 3507253 commit 2c9c664

22 files changed

+16971
-32
lines changed

README.md

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,160 @@
11
# react-native-interactive-tutorial
22

3-
Interactive tutorial with step by step guide
3+
Interactive tutorial with step-by-step guide
4+
5+
<img src="./media/usage_gif.gif" width="600" alt='usage' />
6+
47

58
## Installation
69

10+
1. Install the main library:
711
```sh
8-
npm install react-native-interactive-tutorial
12+
yarn add react-native-interactive-tutorial
13+
```
14+
15+
2. The library has peer dependencies, you should install them to your project: <br />
16+
⚠️ ! Make sure that you have followed all the steps to install these libraries
17+
```sh
18+
yarn add react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-svg
19+
```
20+
21+
3. (Optional) You should use some storage library.
22+
It can be any library, I use @react-native-async-storage/async-storage for an example.
23+
```sh
24+
yarn add @react-native-async-storage/async-storage
925
```
1026

1127
## Usage
28+
Full example you [can find here](./example)
29+
30+
We can divide the usage into 4 parts:
31+
1. Creating the root component in your project:
32+
33+
InteractiveTutorial.tsx:
34+
```tsx
35+
import { type PropsWithChildren, useCallback, useMemo } from 'react';
36+
37+
import AsyncStorage from '@react-native-async-storage/async-storage';
38+
import { Button } from 'react-native';
39+
40+
import {
41+
type DescriptionCardProps,
42+
type SharedDescriptionCardButtonProps,
43+
InteractiveTutorialContainer,
44+
SharedDescriptionCard,
45+
} from 'react-native-interactive-tutorial';
46+
1247

48+
export enum TARGETS {
49+
Target1,
50+
Target2,
51+
Target3,
52+
}
1353

14-
```js
15-
import { multiply } from 'react-native-interactive-tutorial';
54+
export default function InteractiveTutorial({ children }: PropsWithChildren) {
55+
// !! Here you can use different library
56+
const storage = useMemo(
57+
() => ({
58+
set: (_: boolean) => AsyncStorage.setItem('tutorial', String(_)),
59+
get: () => AsyncStorage.getItem('tutorial').then((_) => !!_),
60+
}),
61+
[]
62+
);
1663

17-
// ...
64+
// !! Here are description be key dictionary
65+
const stack = useMemo(
66+
() =>
67+
new Map([
68+
[TARGETS.Target1, 'Target 1'],
69+
[TARGETS.Target2, 'Target 2'],
70+
[TARGETS.Target3, 'Target 3'],
71+
]),
72+
[]
73+
);
1874

19-
const result = await multiply(3, 7);
75+
// !! Translations (for description card)
76+
const translations = useMemo(
77+
() => ({
78+
prevButton: 'Prev',
79+
nextButton: 'Next',
80+
finishButton: 'Finish',
81+
}),
82+
[]
83+
);
84+
85+
return (
86+
<InteractiveTutorialContainer
87+
translations={translations}
88+
stack={stack}
89+
initialTarget={TARGETS.Target1}
90+
Card={DescriptionCard}
91+
storage={storage}
92+
>
93+
{children}
94+
</InteractiveTutorialContainer>
95+
);
96+
}
97+
98+
// !! Here you can override description card with your own
99+
const DescriptionCard = (props: DescriptionCardProps) => {
100+
const DescriptionButton = useCallback(
101+
({ type, ...rest }: SharedDescriptionCardButtonProps) => (
102+
<Button {...rest} color={type === 'prev' ? 'darkblue' : 'blue'} />
103+
),
104+
[]
105+
);
106+
return <SharedDescriptionCard Button={DescriptionButton} {...props} />;
107+
};
108+
109+
```
110+
111+
2. Wrapping your accented components
112+
```tsx
113+
// any places in your app
114+
const target1 = useUiElement(TARGETS.Target1, (_) => addBorderRadius(_, 10));
115+
<View
116+
style={[styles.column, styles.card]}
117+
ref={target1.ref} // !! necessary prop
118+
onLayout={target1.onLayout} // !! necessary prop
119+
>
120+
<Text>Target 1</Text>
121+
</View>
122+
```
123+
124+
3. Creating hook to run the tutorial
125+
```tsx
126+
import { useEffect } from 'react';
127+
import { useInteractiveTutorial } from 'react-native-interactive-tutorial';
128+
129+
export default function useTutorialRunner() {
130+
const tutorial = useInteractiveTutorial();
131+
132+
useEffect(() => {
133+
if (tutorial.finished === false) {
134+
setTimeout(() => tutorial.show());
135+
}
136+
}, [tutorial]);
137+
}
138+
```
139+
140+
4. Wrapping your screen or app in the component from step1 and call the hook from step 3:
141+
```tsx
142+
function Root() {
143+
return (
144+
<SafeAreaProvider> // !! it's also necessary
145+
<InteractiveTutorial> // !! created component from step 1
146+
<App />
147+
</InteractiveTutorial>
148+
</SafeAreaProvider>
149+
);
150+
}
151+
```
152+
Call the hook **inside** the App:
153+
```tsx
154+
function App() {
155+
useTutorialRunner();
156+
...
157+
}
20158
```
21159

22160

example/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@
1010
},
1111
"dependencies": {
1212
"@expo/metro-runtime": "~3.2.3",
13+
"@react-native-async-storage/async-storage": "^2.0.0",
1314
"expo": "~51.0.28",
1415
"expo-status-bar": "~1.12.1",
1516
"react": "18.2.0",
1617
"react-dom": "18.2.0",
1718
"react-native": "0.74.5",
19+
"react-native-gesture-handler": "^2.18.1",
20+
"react-native-reanimated": "^3.15.0",
21+
"react-native-safe-area-context": "^4.10.9",
22+
"react-native-svg": "^15.6.0",
1823
"react-native-web": "~0.19.10"
1924
},
2025
"devDependencies": {

example/src/App.tsx

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,79 @@
1-
import { useState, useEffect } from 'react';
21
import { StyleSheet, View, Text } from 'react-native';
3-
import { multiply } from 'react-native-interactive-tutorial';
2+
import InteractiveTutorial, { TARGETS } from './InteractiveTutorial';
3+
import {
4+
addBorderRadius,
5+
addSize,
6+
useUiElement,
7+
} from 'react-native-interactive-tutorial';
8+
import useTutorialRunner from './useTutorialRunner';
9+
import { SafeAreaProvider } from 'react-native-safe-area-context';
410

5-
export default function App() {
6-
const [result, setResult] = useState<number | undefined>();
11+
export default function Root() {
12+
return (
13+
<SafeAreaProvider>
14+
<InteractiveTutorial>
15+
<App />
16+
</InteractiveTutorial>
17+
</SafeAreaProvider>
18+
);
19+
}
720

8-
useEffect(() => {
9-
multiply(3, 7).then(setResult);
10-
}, []);
21+
function App() {
22+
useTutorialRunner();
23+
24+
const target1 = useUiElement(TARGETS.Target1, (_) => addBorderRadius(_, 10));
25+
const target2 = useUiElement(TARGETS.Target2, (_) =>
26+
addSize(addBorderRadius(_, 10), 30)
27+
);
28+
const target3 = useUiElement(TARGETS.Target3, (_) => addBorderRadius(_, 10));
1129

1230
return (
13-
<View style={styles.container}>
14-
<Text>Result: {result}</Text>
31+
<View style={[styles.root, styles.column]}>
32+
<View style={styles.column}>
33+
<View style={styles.row}>
34+
<View
35+
style={[styles.column, styles.card]}
36+
ref={target1.ref}
37+
onLayout={target1.onLayout}
38+
>
39+
<Text>Target 1</Text>
40+
</View>
41+
<View
42+
style={[styles.column, styles.card]}
43+
ref={target2.ref}
44+
onLayout={target2.onLayout}
45+
>
46+
<Text>Target 3</Text>
47+
</View>
48+
</View>
49+
<View
50+
ref={target3.ref}
51+
onLayout={target3.onLayout}
52+
style={[styles.column, styles.card, { flex: 2 }]}
53+
>
54+
<Text>Target 2</Text>
55+
</View>
56+
</View>
1557
</View>
1658
);
1759
}
1860

1961
const styles = StyleSheet.create({
20-
container: {
62+
root: {
63+
padding: 50,
64+
},
65+
column: {
66+
flexDirection: 'column',
67+
flex: 1,
68+
},
69+
row: {
70+
flexDirection: 'row',
2171
flex: 1,
22-
alignItems: 'center',
23-
justifyContent: 'center',
2472
},
25-
box: {
26-
width: 60,
27-
height: 60,
28-
marginVertical: 20,
73+
card: {
74+
borderWidth: 1,
75+
padding: 10,
76+
justifyContent: 'center',
77+
alignItems: 'center',
2978
},
3079
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { type PropsWithChildren, useCallback, useMemo } from 'react';
2+
3+
import {
4+
type DescriptionCardProps,
5+
type SharedDescriptionCardButtonProps,
6+
InteractiveTutorialContainer,
7+
SharedDescriptionCard,
8+
} from 'react-native-interactive-tutorial';
9+
10+
import AsyncStorage from '@react-native-async-storage/async-storage';
11+
import { Button } from 'react-native';
12+
13+
export enum TARGETS {
14+
Target1,
15+
Target2,
16+
Target3,
17+
}
18+
19+
export default function InteractiveTutorial({ children }: PropsWithChildren) {
20+
const storage = useMemo(
21+
() => ({
22+
set: (_: boolean) => AsyncStorage.setItem('tutorial', String(_)),
23+
get: () => AsyncStorage.getItem('tutorial').then((_) => !!_),
24+
}),
25+
[]
26+
);
27+
28+
const stack = useMemo(
29+
() =>
30+
new Map([
31+
[TARGETS.Target1, 'Target 1'],
32+
[TARGETS.Target2, 'Target 2'],
33+
[TARGETS.Target3, 'Target 3'],
34+
]),
35+
[]
36+
);
37+
38+
const translations = useMemo(
39+
() => ({
40+
prevButton: 'Prev',
41+
nextButton: 'Next',
42+
finishButton: 'Finish',
43+
}),
44+
[]
45+
);
46+
47+
return (
48+
<InteractiveTutorialContainer
49+
translations={translations}
50+
stack={stack}
51+
initialTarget={TARGETS.Target1}
52+
Card={DescriptionCard}
53+
storage={storage}
54+
>
55+
{children}
56+
</InteractiveTutorialContainer>
57+
);
58+
}
59+
60+
const DescriptionCard = (props: DescriptionCardProps) => {
61+
const DescriptionButton = useCallback(
62+
({ type, ...rest }: SharedDescriptionCardButtonProps) => (
63+
<Button {...rest} color={type === 'prev' ? 'darkblue' : 'blue'} />
64+
),
65+
[]
66+
);
67+
return <SharedDescriptionCard Button={DescriptionButton} {...props} />;
68+
};

example/src/useTutorialRunner.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { useEffect } from 'react';
2+
import { useInteractiveTutorial } from 'react-native-interactive-tutorial';
3+
4+
export default function useTutorialRunner() {
5+
const tutorial = useInteractiveTutorial();
6+
7+
useEffect(() => {
8+
if (tutorial.finished === false) {
9+
setTimeout(() => tutorial.show());
10+
}
11+
}, [tutorial]);
12+
}

media/usage_gif.gif

4.11 MB
Loading

0 commit comments

Comments
 (0)