react-native-app-onboard 0.1.9 → 0.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.
Files changed (80) hide show
  1. package/README.md +89 -7
  2. package/lib/commonjs/components/CustomPages.js +31 -55
  3. package/lib/commonjs/components/CustomPages.js.map +1 -1
  4. package/lib/commonjs/components/OnboardingPages.js +63 -79
  5. package/lib/commonjs/components/OnboardingPages.js.map +1 -1
  6. package/lib/commonjs/components/Page.js +8 -3
  7. package/lib/commonjs/components/Page.js.map +1 -1
  8. package/lib/commonjs/components/Pagination.js +75 -13
  9. package/lib/commonjs/components/Pagination.js.map +1 -1
  10. package/lib/commonjs/components/Swiper.js +58 -85
  11. package/lib/commonjs/components/Swiper.js.map +1 -1
  12. package/lib/commonjs/components/button.js +3 -1
  13. package/lib/commonjs/components/button.js.map +1 -1
  14. package/lib/commonjs/context/OnboardingContext.js +101 -21
  15. package/lib/commonjs/context/OnboardingContext.js.map +1 -1
  16. package/lib/commonjs/hooks/useOnboarding.js +1 -1
  17. package/lib/commonjs/hooks/useOnboarding.js.map +1 -1
  18. package/lib/commonjs/index.js +33 -2
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/utils/color.js +308 -0
  21. package/lib/commonjs/utils/color.js.map +1 -0
  22. package/lib/commonjs/utils/persistence.js +51 -0
  23. package/lib/commonjs/utils/persistence.js.map +1 -0
  24. package/lib/module/components/CustomPages.js +31 -55
  25. package/lib/module/components/CustomPages.js.map +1 -1
  26. package/lib/module/components/OnboardingPages.js +64 -79
  27. package/lib/module/components/OnboardingPages.js.map +1 -1
  28. package/lib/module/components/Page.js +8 -3
  29. package/lib/module/components/Page.js.map +1 -1
  30. package/lib/module/components/Pagination.js +76 -14
  31. package/lib/module/components/Pagination.js.map +1 -1
  32. package/lib/module/components/Swiper.js +59 -86
  33. package/lib/module/components/Swiper.js.map +1 -1
  34. package/lib/module/components/button.js +3 -1
  35. package/lib/module/components/button.js.map +1 -1
  36. package/lib/module/context/OnboardingContext.js +102 -22
  37. package/lib/module/context/OnboardingContext.js.map +1 -1
  38. package/lib/module/hooks/useOnboarding.js +1 -1
  39. package/lib/module/hooks/useOnboarding.js.map +1 -1
  40. package/lib/module/index.js +8 -1
  41. package/lib/module/index.js.map +1 -1
  42. package/lib/module/utils/color.js +299 -0
  43. package/lib/module/utils/color.js.map +1 -0
  44. package/lib/module/utils/persistence.js +42 -0
  45. package/lib/module/utils/persistence.js.map +1 -0
  46. package/lib/typescript/src/components/CustomPages.d.ts +6 -2
  47. package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
  48. package/lib/typescript/src/components/OnboardingPages.d.ts +6 -2
  49. package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
  50. package/lib/typescript/src/components/Page.d.ts +2 -0
  51. package/lib/typescript/src/components/Page.d.ts.map +1 -1
  52. package/lib/typescript/src/components/Pagination.d.ts +9 -0
  53. package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
  54. package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
  55. package/lib/typescript/src/components/button.d.ts.map +1 -1
  56. package/lib/typescript/src/context/OnboardingContext.d.ts +9 -0
  57. package/lib/typescript/src/context/OnboardingContext.d.ts.map +1 -1
  58. package/lib/typescript/src/hooks/useOnboarding.d.ts +3 -0
  59. package/lib/typescript/src/hooks/useOnboarding.d.ts.map +1 -1
  60. package/lib/typescript/src/index.d.ts +4 -0
  61. package/lib/typescript/src/index.d.ts.map +1 -1
  62. package/lib/typescript/src/types/index.d.ts +13 -0
  63. package/lib/typescript/src/types/index.d.ts.map +1 -1
  64. package/lib/typescript/src/utils/color.d.ts +18 -0
  65. package/lib/typescript/src/utils/color.d.ts.map +1 -0
  66. package/lib/typescript/src/utils/persistence.d.ts +31 -0
  67. package/lib/typescript/src/utils/persistence.d.ts.map +1 -0
  68. package/package.json +12 -6
  69. package/src/components/CustomPages.tsx +62 -69
  70. package/src/components/OnboardingPages.tsx +86 -89
  71. package/src/components/Page.tsx +8 -2
  72. package/src/components/Pagination.tsx +117 -29
  73. package/src/components/Swiper.tsx +65 -87
  74. package/src/components/button.tsx +6 -1
  75. package/src/context/OnboardingContext.tsx +145 -26
  76. package/src/hooks/useOnboarding.tsx +1 -3
  77. package/src/index.tsx +16 -1
  78. package/src/types/index.ts +13 -0
  79. package/src/utils/color.ts +284 -0
  80. package/src/utils/persistence.ts +58 -0
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  View,
3
+ Pressable,
3
4
  Animated,
4
5
  StyleSheet,
5
6
  type StyleProp,
@@ -7,6 +8,7 @@ import {
7
8
  type TextStyle,
8
9
  } from 'react-native';
9
10
  import React from 'react';
11
+ import { setAlpha } from '../utils/color';
10
12
  import { useOnboarding } from '../hooks/useOnboarding';
11
13
  import { Button } from './button';
12
14
 
@@ -19,9 +21,11 @@ type FooterProps = {
19
21
  showDone?: boolean;
20
22
  showSkip?: boolean;
21
23
  showNext?: boolean;
24
+ showPrevious?: boolean;
22
25
  nextLabel?: string | React.ReactNode;
23
26
  skipLabel?: string | React.ReactNode;
24
27
  doneLabel?: string | React.ReactNode;
28
+ previousLabel?: string | React.ReactNode;
25
29
  paginationContainerStyle?: StyleProp<ViewStyle>;
26
30
  buttonRightContainerStyle?: StyleProp<ViewStyle>;
27
31
  buttonLeftContainerStyle?: StyleProp<ViewStyle>;
@@ -29,20 +33,30 @@ type FooterProps = {
29
33
  doneLabelStyle?: StyleProp<TextStyle>;
30
34
  hasSkipPosition?: boolean;
31
35
  skipLabelStyle?: StyleProp<TextStyle>;
36
+ previousLabelStyle?: StyleProp<TextStyle>;
32
37
  skipButtonContainerStyle?: StyleProp<ViewStyle>;
33
38
  nextButtonContainerStyle?: StyleProp<ViewStyle>;
34
39
  doneButtonContainerStyle?: StyleProp<ViewStyle>;
40
+ previousButtonContainerStyle?: StyleProp<ViewStyle>;
35
41
  nextLabelStyle?: StyleProp<TextStyle>;
36
42
  paginationPosition?: 'top' | 'bottom';
43
+ paginationStyle?: 'dots' | 'progress';
44
+ progressBarStyle?: StyleProp<ViewStyle>;
45
+ progressBarFillStyle?: StyleProp<ViewStyle>;
46
+ dotsAreTappable?: boolean;
47
+ mirror?: boolean;
37
48
  onDone?: () => void;
38
49
  onSkip?: () => void;
39
50
  onNext?: () => void;
40
51
  };
41
52
 
42
53
  export function Pagination(props: FooterProps) {
43
- const { isDone } = useOnboarding();
54
+ const { isDone, currentPage, progress, scrollTo, previousPage } =
55
+ useOnboarding();
44
56
  const dots = Array.from({ length: props.numberOfScreens }, (_, i) => i);
45
57
  const width = props.width;
58
+ const showPrevious = props.showPrevious && currentPage > 0;
59
+
46
60
  return (
47
61
  <View
48
62
  style={[
@@ -60,7 +74,15 @@ export function Pagination(props: FooterProps) {
60
74
  props.buttonLeftContainerStyle,
61
75
  ]}
62
76
  >
63
- {props.showSkip && !props.hasSkipPosition && (
77
+ {showPrevious && (
78
+ <Button
79
+ onPress={() => previousPage()}
80
+ buttonTextStyle={props.previousLabelStyle}
81
+ buttonStyle={props.previousButtonContainerStyle}
82
+ label={props.previousLabel || 'Back'}
83
+ />
84
+ )}
85
+ {!showPrevious && props.showSkip && !props.hasSkipPosition && (
64
86
  <Button
65
87
  onPress={props.onSkip}
66
88
  buttonTextStyle={props.skipLabelStyle}
@@ -69,29 +91,82 @@ export function Pagination(props: FooterProps) {
69
91
  />
70
92
  )}
71
93
  </View>
72
- <View style={styles.dotsContainer}>
73
- {dots.map((_, i) => {
74
- const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
75
- const dotOpacity = props.animatedValue.interpolate({
76
- inputRange,
77
- outputRange: [0.3, 1, 0.3],
78
- extrapolate: 'clamp',
79
- });
80
- return (
81
- <Animated.View
82
- key={i}
83
- style={[
84
- styles.dot,
85
- {
86
- backgroundColor: props.color,
87
- opacity: dotOpacity,
88
- },
89
- props.dotsContainerStyle,
90
- ]}
91
- />
92
- );
93
- })}
94
- </View>
94
+
95
+ {props.paginationStyle === 'progress' ? (
96
+ <View
97
+ accessible
98
+ accessibilityRole="progressbar"
99
+ accessibilityValue={{ min: 0, max: 100, now: progress }}
100
+ style={[
101
+ styles.progressTrack,
102
+ // Derive a faint track from the (background-aware) fill color so it
103
+ // stays visible on both light and dark pages.
104
+ { backgroundColor: setAlpha(props.color, 0.25) },
105
+ // Mirror the fill direction so it grows from the trailing edge.
106
+ props.mirror && styles.mirror,
107
+ props.progressBarStyle,
108
+ ]}
109
+ >
110
+ <View
111
+ style={[
112
+ styles.progressFill,
113
+ { backgroundColor: props.color, width: `${progress}%` },
114
+ props.progressBarFillStyle,
115
+ ]}
116
+ />
117
+ </View>
118
+ ) : (
119
+ <View
120
+ style={[
121
+ styles.dotsContainer,
122
+ props.mirror && styles.mirror,
123
+ props.dotsContainerStyle,
124
+ ]}
125
+ >
126
+ {dots.map((_, i) => {
127
+ const inputRange = [(i - 1) * width, i * width, (i + 1) * width];
128
+ const dotOpacity = props.animatedValue.interpolate({
129
+ inputRange,
130
+ outputRange: [0.3, 1, 0.3],
131
+ extrapolate: 'clamp',
132
+ });
133
+ const dotProps = {
134
+ accessibilityRole: props.dotsAreTappable
135
+ ? ('button' as const)
136
+ : ('image' as const),
137
+ accessibilityLabel: `Page ${i + 1} of ${props.numberOfScreens}`,
138
+ accessibilityState: { selected: i === currentPage },
139
+ };
140
+ const dot = (
141
+ <Animated.View
142
+ key={i}
143
+ style={[
144
+ styles.dot,
145
+ {
146
+ backgroundColor: props.color,
147
+ opacity: dotOpacity,
148
+ },
149
+ ]}
150
+ />
151
+ );
152
+ return props.dotsAreTappable ? (
153
+ <Pressable
154
+ key={i}
155
+ onPress={() => scrollTo(i)}
156
+ hitSlop={8}
157
+ {...dotProps}
158
+ >
159
+ {dot}
160
+ </Pressable>
161
+ ) : (
162
+ <View key={i} {...dotProps}>
163
+ {dot}
164
+ </View>
165
+ );
166
+ })}
167
+ </View>
168
+ )}
169
+
95
170
  <View
96
171
  style={[
97
172
  styles.buttons,
@@ -138,21 +213,34 @@ const styles = StyleSheet.create({
138
213
  },
139
214
  dotsContainer: {
140
215
  flexDirection: 'row',
141
- flex: 1,
216
+ flex: 2,
142
217
  justifyContent: 'center',
218
+ alignItems: 'center',
219
+ },
220
+ progressTrack: {
221
+ flex: 2,
222
+ height: 6,
223
+ borderRadius: 3,
224
+ marginHorizontal: 16,
225
+ overflow: 'hidden',
226
+ },
227
+ progressFill: {
228
+ height: '100%',
229
+ borderRadius: 3,
143
230
  },
144
231
  text: {
145
232
  fontSize: 16,
146
233
  },
147
234
  buttons: {
148
- minWidth: 200,
235
+ flex: 1,
149
236
  },
150
237
  rightButton: {
151
238
  alignItems: 'flex-end',
152
- paddingRight: 30,
153
239
  },
154
240
  leftButton: {
155
241
  alignItems: 'flex-start',
156
- paddingLeft: 30,
242
+ },
243
+ mirror: {
244
+ transform: [{ scaleX: -1 }],
157
245
  },
158
246
  });
@@ -1,109 +1,87 @@
1
1
  import React from 'react';
2
2
  import { OnboardingPages } from './OnboardingPages';
3
3
  import { CustomPages } from './CustomPages';
4
- import { Animated } from 'react-native';
4
+ import {
5
+ Animated,
6
+ I18nManager,
7
+ type NativeSyntheticEvent,
8
+ type NativeScrollEvent,
9
+ } from 'react-native';
5
10
  import { useOnboarding } from '../hooks/useOnboarding';
6
11
  import type { OnboardingProps } from '../types';
7
12
 
8
13
  export const Swiper: React.FC<OnboardingProps> = (props) => {
9
- const scrollX = React.useRef(new Animated.Value(0)).current;
14
+ // `scrollX` is always JS-driven because the background-color interpolation
15
+ // cannot run on the native thread. When `useNativeDriver` is enabled we also
16
+ // keep `nativeScrollX`, driven natively, for transform/opacity animations
17
+ // (the pagination dots), and mirror its offset onto `scrollX` via a JS
18
+ // listener so the color interpolation keeps working.
19
+ const scrollX = React.useMemo(() => new Animated.Value(0), []);
20
+ const nativeScrollX = React.useMemo(() => new Animated.Value(0), []);
21
+ const nativeDriverEnabled = props.useNativeDriver ?? false;
22
+ const dotsAnimatedValue = nativeDriverEnabled ? nativeScrollX : scrollX;
23
+
24
+ // Direction handling: default to the device direction. We only mirror
25
+ // manually (via scaleX) when the requested direction differs from the
26
+ // device's — when they match, React Native already lays the row out
27
+ // correctly and an extra flip would double-invert it.
28
+ const rtl = props.rtl ?? I18nManager.isRTL;
29
+ const mirror = rtl !== I18nManager.isRTL;
30
+
10
31
  const {
11
- flatListRef,
32
+ setFlatListRef,
12
33
  setCurrentPage,
13
34
  currentPage,
14
35
  numberOfScreens,
15
36
  nextPage,
16
37
  scrollEnabled,
38
+ pauseAutoPlay,
17
39
  } = useOnboarding();
40
+
41
+ const onScroll = React.useMemo(
42
+ () =>
43
+ nativeDriverEnabled
44
+ ? Animated.event(
45
+ [{ nativeEvent: { contentOffset: { x: nativeScrollX } } }],
46
+ {
47
+ useNativeDriver: true,
48
+ listener: (event: NativeSyntheticEvent<NativeScrollEvent>) =>
49
+ scrollX.setValue(event.nativeEvent.contentOffset.x),
50
+ }
51
+ )
52
+ : Animated.event([{ nativeEvent: { contentOffset: { x: scrollX } } }], {
53
+ useNativeDriver: false,
54
+ }),
55
+ [nativeDriverEnabled, nativeScrollX, scrollX]
56
+ );
57
+
58
+ // Stop autoplay as soon as the user takes manual control of the slider.
59
+ const onScrollBeginDrag = React.useCallback(
60
+ () => pauseAutoPlay(),
61
+ [pauseAutoPlay]
62
+ );
63
+
64
+ const shared = {
65
+ setFlatListRef,
66
+ scrollX,
67
+ dotsAnimatedValue,
68
+ onScroll,
69
+ onScrollBeginDrag,
70
+ setPage: setCurrentPage,
71
+ currentPage,
72
+ numberOfScreens,
73
+ nextPage,
74
+ scrollEnabled,
75
+ mirror,
76
+ };
77
+
18
78
  if (props.children) {
19
79
  return (
20
- <CustomPages
21
- customFooter={props.customFooter}
22
- showPagination={props.showPagination}
23
- flatListRef={flatListRef}
24
- scrollX={scrollX}
25
- setPage={setCurrentPage}
26
- scrollEnabled={scrollEnabled}
27
- currentPage={currentPage}
28
- numberOfScreens={numberOfScreens}
29
- nextPage={nextPage}
30
- showDone={props.showDone}
31
- showNext={props.showNext}
32
- onDone={props.onDone}
33
- skipButtonContainerStyle={props.skipButtonContainerStyle}
34
- nextButtonContainerStyle={props.nextButtonContainerStyle}
35
- doneButtonContainerStyle={props.doneButtonContainerStyle}
36
- skipButtonPosition={props.skipButtonPosition}
37
- paginationContainerStyle={props.paginationContainerStyle}
38
- paginationPosition={props.paginationPosition}
39
- nextLabel={props.nextLabel}
40
- skipLabel={props.skipLabel}
41
- doneLabel={props.doneLabel}
42
- showSkip={props.showSkip}
43
- onSkip={props.onSkip}
44
- scrollAnimationDuration={props.scrollAnimationDuration}
45
- buttonLeftContainerStyle={props.buttonLeftContainerStyle}
46
- buttonRightContainerStyle={props.buttonRightContainerStyle}
47
- dotsContainerStyle={props.dotsContainerStyle}
48
- doneLabelStyle={props.doneLabelStyle}
49
- skipLabelStyle={props.skipLabelStyle}
50
- nextLabelStyle={props.nextLabelStyle}
51
- width={props.width}
52
- color={props.color}
53
- useNativeDriver={props.useNativeDriver}
54
- imageContainerStyle={props.imageContainerStyle}
55
- containerStyle={props.containerStyle}
56
- titleContainerStyle={props.titleContainerStyle}
57
- titleStyle={props.titleStyle}
58
- subtitleStyle={props.subtitleStyle}
59
- swap={props.swap}
60
- >
80
+ <CustomPages {...props} {...shared}>
61
81
  {props.children}
62
82
  </CustomPages>
63
83
  );
64
84
  }
65
85
 
66
- return (
67
- <OnboardingPages
68
- showDone={props.showDone}
69
- customFooter={props.customFooter}
70
- flatListRef={flatListRef}
71
- scrollX={scrollX}
72
- setPage={setCurrentPage}
73
- currentPage={currentPage}
74
- paginationPosition={props.paginationPosition}
75
- nextPage={nextPage}
76
- showSkip={props.showSkip}
77
- onDone={props.onDone}
78
- pages={props.pages || []}
79
- width={props.width}
80
- showNext={props.showNext}
81
- skipButtonContainerStyle={props.skipButtonContainerStyle}
82
- nextButtonContainerStyle={props.nextButtonContainerStyle}
83
- doneButtonContainerStyle={props.doneButtonContainerStyle}
84
- skipLabelStyle={props.skipLabelStyle}
85
- skipButtonPosition={props.skipButtonPosition}
86
- showPagination={props.showPagination}
87
- color={props.color}
88
- onSkip={props.onSkip}
89
- swap={props.swap}
90
- scrollEnabled={scrollEnabled}
91
- nextLabel={props.nextLabel}
92
- skipLabel={props.skipLabel}
93
- doneLabel={props.doneLabel}
94
- scrollAnimationDuration={props.scrollAnimationDuration}
95
- buttonLeftContainerStyle={props.buttonLeftContainerStyle}
96
- buttonRightContainerStyle={props.buttonRightContainerStyle}
97
- dotsContainerStyle={props.dotsContainerStyle}
98
- doneLabelStyle={props.doneLabelStyle}
99
- nextLabelStyle={props.nextLabelStyle}
100
- useNativeDriver={props.useNativeDriver}
101
- imageContainerStyle={props.imageContainerStyle}
102
- containerStyle={props.containerStyle}
103
- titleContainerStyle={props.titleContainerStyle}
104
- titleStyle={props.titleStyle}
105
- subtitleStyle={props.subtitleStyle}
106
- paginationContainerStyle={props.paginationContainerStyle}
107
- />
108
- );
86
+ return <OnboardingPages {...props} {...shared} pages={props.pages || []} />;
109
87
  };
@@ -18,7 +18,12 @@ type ButtonProps = {
18
18
 
19
19
  export const Button = (props: ButtonProps) => {
20
20
  return typeof props.label === 'string' ? (
21
- <TouchableOpacity onPress={props.onPress} style={props.buttonStyle}>
21
+ <TouchableOpacity
22
+ onPress={props.onPress}
23
+ style={props.buttonStyle}
24
+ accessibilityRole="button"
25
+ accessibilityLabel={props.label}
26
+ >
22
27
  <Text
23
28
  style={[
24
29
  styles.text,
@@ -1,22 +1,35 @@
1
1
  import React from 'react';
2
- import { Dimensions, type FlatList } from 'react-native';
2
+ import {
3
+ AccessibilityInfo,
4
+ Animated,
5
+ Dimensions,
6
+ type FlatList,
7
+ } from 'react-native';
3
8
 
4
9
  export type SliderProps = {
5
10
  currentPage: number;
6
11
  numberOfScreens: number;
7
12
  nextPage: (animated?: boolean) => void;
13
+ previousPage: (animated?: boolean) => void;
8
14
  scrollTo: (index: number, animated?: boolean) => void;
9
15
  };
10
16
 
11
17
  type OnboardingContextType = SliderProps & {
12
18
  setCurrentPage: (index: number) => void;
13
19
  flatListRef: React.RefObject<FlatList>;
20
+ // Callback ref forwarded to the underlying FlatList. Exposing a setter (vs.
21
+ // the ref object itself) keeps consumers React-Compiler-safe: assigning a
22
+ // foreign ref object to a `ref` prop is flagged as "ref access during
23
+ // render", whereas a callback ref is not.
24
+ setFlatListRef: (node: FlatList | null) => void;
14
25
  width?: number;
15
26
  numberOfScreens: number;
16
27
  progress: number;
17
28
  scrollEnabled?: boolean;
18
29
  enableScroll: React.Dispatch<React.SetStateAction<boolean | undefined>>;
19
30
  isDone: boolean;
31
+ pauseAutoPlay: () => void;
32
+ resumeAutoPlay: () => void;
20
33
  };
21
34
 
22
35
  type OnboardingProviderProps = {
@@ -24,6 +37,11 @@ type OnboardingProviderProps = {
24
37
  width?: number;
25
38
  numberOfScreens: number;
26
39
  scrollEnabled?: boolean;
40
+ onPageChange?: (index: number) => void;
41
+ scrollAnimationDuration?: number;
42
+ autoPlay?: boolean;
43
+ autoPlayInterval?: number;
44
+ loop?: boolean;
27
45
  };
28
46
 
29
47
  export const OnboardingContext = React.createContext<
@@ -32,10 +50,16 @@ export const OnboardingContext = React.createContext<
32
50
 
33
51
  export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({
34
52
  children,
35
- width = Dimensions.get('window').width,
53
+ width: widthProp,
36
54
  numberOfScreens,
37
55
  scrollEnabled,
56
+ onPageChange,
57
+ scrollAnimationDuration,
58
+ autoPlay = false,
59
+ autoPlayInterval = 3000,
60
+ loop = false,
38
61
  }) => {
62
+ const width = widthProp ?? Dimensions.get('window').width;
39
63
  const getProgress = (page: number) => {
40
64
  return Math.round(((page + 1) / numberOfScreens) * 100);
41
65
  };
@@ -46,45 +70,140 @@ export const OnboardingProvider: React.FC<OnboardingProviderProps> = ({
46
70
  const [enableScroll, setEnableScroll] = React.useState<boolean | undefined>(
47
71
  scrollEnabled
48
72
  );
49
- const flatListRef = React.useRef<FlatList>(null);
73
+ const flatListRef = React.useRef<FlatList | null>(null);
74
+ const setFlatListRef = React.useCallback((node: FlatList | null) => {
75
+ flatListRef.current = node;
76
+ }, []);
77
+ // Tracks the latest page so timers/animations read a fresh value without
78
+ // needing to be recreated on every page change.
79
+ const currentPageRef = React.useRef(0);
80
+ const [isAutoPlaying, setIsAutoPlaying] = React.useState(autoPlay);
50
81
 
51
- const setCurrentPage = (index: number) => {
52
- setPage(index);
53
- setProgress(getProgress(index));
54
- setIsDone(index === numberOfScreens - 1);
55
- };
82
+ // Keep autoplay in sync if the `autoPlay` prop is toggled after mount.
83
+ React.useEffect(() => {
84
+ setIsAutoPlaying(autoPlay);
85
+ }, [autoPlay]);
56
86
 
57
- const nextPage = (animated: boolean = true) => {
58
- if (flatListRef.current && currentPage < numberOfScreens - 1) {
59
- flatListRef.current.scrollToOffset({
60
- offset: width * (currentPage + 1),
61
- animated: animated,
62
- });
63
- setCurrentPage(currentPage + 1);
64
- }
65
- };
87
+ // Dedicated value used to honor a custom scrollAnimationDuration. FlatList's
88
+ // own animated scroll has a fixed, platform-controlled duration, so when a
89
+ // duration is requested we drive the offset manually via this value.
90
+ const scrollAnim = React.useMemo(() => new Animated.Value(0), []);
91
+ React.useEffect(() => {
92
+ const id = scrollAnim.addListener(({ value }) => {
93
+ flatListRef.current?.scrollToOffset({ offset: value, animated: false });
94
+ });
95
+ return () => scrollAnim.removeListener(id);
96
+ }, [scrollAnim]);
66
97
 
67
- const scrollTo = (index: number, animated: boolean = true) => {
68
- if (flatListRef.current && index >= 0) {
69
- flatListRef.current.scrollToOffset({
70
- offset: index * width,
71
- animated: animated,
72
- });
73
- setCurrentPage(index);
74
- }
75
- };
98
+ const animateToOffset = React.useCallback(
99
+ (offset: number, animated: boolean) => {
100
+ if (!flatListRef.current) return;
101
+ if (animated && scrollAnimationDuration) {
102
+ scrollAnim.stopAnimation();
103
+ scrollAnim.setValue(width * currentPageRef.current);
104
+ Animated.timing(scrollAnim, {
105
+ toValue: offset,
106
+ duration: scrollAnimationDuration,
107
+ useNativeDriver: false,
108
+ }).start();
109
+ } else {
110
+ flatListRef.current.scrollToOffset({ offset, animated });
111
+ }
112
+ },
113
+ [scrollAnim, scrollAnimationDuration, width]
114
+ );
115
+
116
+ const setCurrentPage = React.useCallback(
117
+ (index: number) => {
118
+ setPage(index);
119
+ currentPageRef.current = index;
120
+ setProgress(Math.round(((index + 1) / numberOfScreens) * 100));
121
+ setIsDone(index === numberOfScreens - 1);
122
+ onPageChange?.(index);
123
+ // No-op when no screen reader is active; announces the page otherwise.
124
+ AccessibilityInfo.announceForAccessibility(
125
+ `Page ${index + 1} of ${numberOfScreens}`
126
+ );
127
+ },
128
+ [numberOfScreens, onPageChange]
129
+ );
130
+
131
+ const nextPage = React.useCallback(
132
+ (animated: boolean = true) => {
133
+ const current = currentPageRef.current;
134
+ if (current < numberOfScreens - 1) {
135
+ animateToOffset(width * (current + 1), animated);
136
+ setCurrentPage(current + 1);
137
+ } else if (loop) {
138
+ animateToOffset(0, animated);
139
+ setCurrentPage(0);
140
+ }
141
+ },
142
+ [numberOfScreens, loop, width, animateToOffset, setCurrentPage]
143
+ );
144
+
145
+ const previousPage = React.useCallback(
146
+ (animated: boolean = true) => {
147
+ const current = currentPageRef.current;
148
+ if (current > 0) {
149
+ animateToOffset(width * (current - 1), animated);
150
+ setCurrentPage(current - 1);
151
+ } else if (loop) {
152
+ animateToOffset(width * (numberOfScreens - 1), animated);
153
+ setCurrentPage(numberOfScreens - 1);
154
+ }
155
+ },
156
+ [numberOfScreens, loop, width, animateToOffset, setCurrentPage]
157
+ );
158
+
159
+ const scrollTo = React.useCallback(
160
+ (index: number, animated: boolean = true) => {
161
+ if (index >= 0 && index < numberOfScreens) {
162
+ animateToOffset(index * width, animated);
163
+ setCurrentPage(index);
164
+ }
165
+ },
166
+ [numberOfScreens, width, animateToOffset, setCurrentPage]
167
+ );
168
+
169
+ const pauseAutoPlay = React.useCallback(() => setIsAutoPlaying(false), []);
170
+ const resumeAutoPlay = React.useCallback(
171
+ () => setIsAutoPlaying(autoPlay),
172
+ [autoPlay]
173
+ );
174
+
175
+ // Autoplay timer. Recreated whenever the page changes so it always advances
176
+ // from the current position; pauses when the user interacts with the slider.
177
+ React.useEffect(() => {
178
+ if (!isAutoPlaying || numberOfScreens <= 1) return;
179
+ if (!loop && currentPage >= numberOfScreens - 1) return;
180
+ const timer = setTimeout(() => nextPage(true), autoPlayInterval);
181
+ return () => clearTimeout(timer);
182
+ }, [
183
+ isAutoPlaying,
184
+ currentPage,
185
+ numberOfScreens,
186
+ autoPlayInterval,
187
+ loop,
188
+ nextPage,
189
+ ]);
76
190
 
77
191
  const contextValue: OnboardingContextType = {
78
192
  scrollEnabled: enableScroll,
79
193
  enableScroll: setEnableScroll,
194
+ width,
80
195
  currentPage,
81
196
  numberOfScreens,
82
197
  nextPage,
198
+ previousPage,
83
199
  setCurrentPage,
84
200
  flatListRef,
201
+ setFlatListRef,
85
202
  scrollTo,
86
203
  progress,
87
204
  isDone,
205
+ pauseAutoPlay,
206
+ resumeAutoPlay,
88
207
  };
89
208
 
90
209
  return (
@@ -4,9 +4,7 @@ import { OnboardingContext } from '../context/OnboardingContext';
4
4
  export const useOnboarding = () => {
5
5
  const context = React.useContext(OnboardingContext);
6
6
  if (!context) {
7
- throw new Error(
8
- 'useOnboardingContext must be used within an OnboardingProvider'
9
- );
7
+ throw new Error('useOnboarding must be used within an OnboardingProvider');
10
8
  }
11
9
  return context;
12
10
  };
package/src/index.tsx CHANGED
@@ -1,15 +1,30 @@
1
1
  import React from 'react';
2
- import { Swiper } from './components/Swiper';
2
+ import { Swiper } from './components';
3
3
  import { OnboardingProvider } from './context/OnboardingContext';
4
4
  import type { OnboardingProps } from './types';
5
5
 
6
6
  export { useOnboarding } from './hooks/useOnboarding';
7
+ export type { Page } from './components/Page';
8
+ export type { OnboardingProps } from './types';
9
+ export {
10
+ createOnboardingStorage,
11
+ hasCompletedOnboarding,
12
+ markOnboardingComplete,
13
+ resetOnboarding,
14
+ } from './utils/persistence';
15
+ export type { OnboardingStorageAdapter } from './utils/persistence';
7
16
 
8
17
  export function Onboarding(props: OnboardingProps) {
9
18
  const numberOfScreens = React.Children.count(props.children);
10
19
  return (
11
20
  <OnboardingProvider
21
+ width={props.width}
12
22
  scrollEnabled={props.scrollEnabled}
23
+ onPageChange={props.onPageChange}
24
+ scrollAnimationDuration={props.scrollAnimationDuration}
25
+ autoPlay={props.autoPlay}
26
+ autoPlayInterval={props.autoPlayInterval}
27
+ loop={props.loop}
13
28
  numberOfScreens={numberOfScreens || props.pages?.length || 0}
14
29
  >
15
30
  <Swiper {...props}>{props.children}</Swiper>