react-native-app-onboard 0.1.9 → 0.2.0

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 (73) 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 +59 -74
  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/persistence.js +51 -0
  21. package/lib/commonjs/utils/persistence.js.map +1 -0
  22. package/lib/module/components/CustomPages.js +31 -55
  23. package/lib/module/components/CustomPages.js.map +1 -1
  24. package/lib/module/components/OnboardingPages.js +60 -75
  25. package/lib/module/components/OnboardingPages.js.map +1 -1
  26. package/lib/module/components/Page.js +8 -3
  27. package/lib/module/components/Page.js.map +1 -1
  28. package/lib/module/components/Pagination.js +76 -14
  29. package/lib/module/components/Pagination.js.map +1 -1
  30. package/lib/module/components/Swiper.js +59 -86
  31. package/lib/module/components/Swiper.js.map +1 -1
  32. package/lib/module/components/button.js +3 -1
  33. package/lib/module/components/button.js.map +1 -1
  34. package/lib/module/context/OnboardingContext.js +102 -22
  35. package/lib/module/context/OnboardingContext.js.map +1 -1
  36. package/lib/module/hooks/useOnboarding.js +1 -1
  37. package/lib/module/hooks/useOnboarding.js.map +1 -1
  38. package/lib/module/index.js +8 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/utils/persistence.js +42 -0
  41. package/lib/module/utils/persistence.js.map +1 -0
  42. package/lib/typescript/src/components/CustomPages.d.ts +6 -2
  43. package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
  44. package/lib/typescript/src/components/OnboardingPages.d.ts +6 -2
  45. package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
  46. package/lib/typescript/src/components/Page.d.ts +2 -0
  47. package/lib/typescript/src/components/Page.d.ts.map +1 -1
  48. package/lib/typescript/src/components/Pagination.d.ts +9 -0
  49. package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
  50. package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
  51. package/lib/typescript/src/components/button.d.ts.map +1 -1
  52. package/lib/typescript/src/context/OnboardingContext.d.ts +9 -0
  53. package/lib/typescript/src/context/OnboardingContext.d.ts.map +1 -1
  54. package/lib/typescript/src/hooks/useOnboarding.d.ts +3 -0
  55. package/lib/typescript/src/hooks/useOnboarding.d.ts.map +1 -1
  56. package/lib/typescript/src/index.d.ts +4 -0
  57. package/lib/typescript/src/index.d.ts.map +1 -1
  58. package/lib/typescript/src/types/index.d.ts +13 -0
  59. package/lib/typescript/src/types/index.d.ts.map +1 -1
  60. package/lib/typescript/src/utils/persistence.d.ts +31 -0
  61. package/lib/typescript/src/utils/persistence.d.ts.map +1 -0
  62. package/package.json +13 -3
  63. package/src/components/CustomPages.tsx +62 -69
  64. package/src/components/OnboardingPages.tsx +79 -82
  65. package/src/components/Page.tsx +8 -2
  66. package/src/components/Pagination.tsx +121 -29
  67. package/src/components/Swiper.tsx +65 -87
  68. package/src/components/button.tsx +6 -1
  69. package/src/context/OnboardingContext.tsx +145 -26
  70. package/src/hooks/useOnboarding.tsx +1 -3
  71. package/src/index.tsx +16 -1
  72. package/src/types/index.ts +13 -0
  73. package/src/utils/persistence.ts +58 -0
@@ -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>
@@ -6,13 +6,19 @@ export type OnboardingProps = {
6
6
  nextLabel?: string | React.ReactNode;
7
7
  skipLabel?: string | React.ReactNode;
8
8
  doneLabel?: string | React.ReactNode;
9
+ previousLabel?: string | React.ReactNode;
9
10
  showSkip?: boolean;
10
11
  showNext?: boolean;
11
12
  showDone?: boolean;
13
+ showPrevious?: boolean;
12
14
  onDone?: () => void;
13
15
  onSkip?: () => void;
16
+ onPageChange?: (index: number) => void;
14
17
  showPagination?: boolean;
15
18
  scrollEnabled?: boolean;
19
+ autoPlay?: boolean;
20
+ autoPlayInterval?: number;
21
+ loop?: boolean;
16
22
  customFooter?: (props: { nextPage: () => void }) => React.ReactNode;
17
23
  paginationContainerStyle?: StyleProp<ViewStyle>;
18
24
  buttonRightContainerStyle?: StyleProp<ViewStyle>;
@@ -20,9 +26,11 @@ export type OnboardingProps = {
20
26
  dotsContainerStyle?: StyleProp<ViewStyle>;
21
27
  doneLabelStyle?: StyleProp<TextStyle>;
22
28
  skipLabelStyle?: StyleProp<TextStyle>;
29
+ previousLabelStyle?: StyleProp<TextStyle>;
23
30
  skipButtonContainerStyle?: StyleProp<ViewStyle>;
24
31
  nextButtonContainerStyle?: StyleProp<ViewStyle>;
25
32
  doneButtonContainerStyle?: StyleProp<ViewStyle>;
33
+ previousButtonContainerStyle?: StyleProp<ViewStyle>;
26
34
  skipButtonPosition?: 'top-left' | 'top-right';
27
35
  nextLabelStyle?: StyleProp<TextStyle>;
28
36
  containerStyle?: StyleProp<ViewStyle>;
@@ -31,8 +39,13 @@ export type OnboardingProps = {
31
39
  titleStyle?: StyleProp<TextStyle>;
32
40
  subtitleStyle?: StyleProp<TextStyle>;
33
41
  paginationPosition?: 'top' | 'bottom';
42
+ paginationStyle?: 'dots' | 'progress';
43
+ progressBarStyle?: StyleProp<ViewStyle>;
44
+ progressBarFillStyle?: StyleProp<ViewStyle>;
45
+ dotsAreTappable?: boolean;
34
46
  scrollAnimationDuration?: number;
35
47
  useNativeDriver?: boolean;
48
+ rtl?: boolean;
36
49
  width?: number;
37
50
  color?: string;
38
51
  pages?: Page[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Minimal AsyncStorage-compatible interface. Any storage that implements these
3
+ * three methods works (e.g. @react-native-async-storage/async-storage,
4
+ * expo-secure-store wrappers, or an in-memory mock in tests). Keeping the
5
+ * dependency injected means this library does not have to ship a storage peer
6
+ * dependency of its own.
7
+ */
8
+ export type OnboardingStorageAdapter = {
9
+ getItem: (key: string) => Promise<string | null>;
10
+ setItem: (key: string, value: string) => Promise<void>;
11
+ removeItem: (key: string) => Promise<void>;
12
+ };
13
+
14
+ const DEFAULT_KEY = '@react-native-app-onboard/completed';
15
+ const COMPLETED_VALUE = 'true';
16
+
17
+ /**
18
+ * Returns whether the user has previously completed onboarding stored under
19
+ * `key`. Defaults to a namespaced key so callers usually only pass the storage.
20
+ */
21
+ export async function hasCompletedOnboarding(
22
+ storage: OnboardingStorageAdapter,
23
+ key: string = DEFAULT_KEY
24
+ ): Promise<boolean> {
25
+ const value = await storage.getItem(key);
26
+ return value === COMPLETED_VALUE;
27
+ }
28
+
29
+ /** Marks onboarding as completed so it can be skipped on subsequent launches. */
30
+ export async function markOnboardingComplete(
31
+ storage: OnboardingStorageAdapter,
32
+ key: string = DEFAULT_KEY
33
+ ): Promise<void> {
34
+ await storage.setItem(key, COMPLETED_VALUE);
35
+ }
36
+
37
+ /** Clears the stored completion flag (useful for "replay onboarding" actions). */
38
+ export async function resetOnboarding(
39
+ storage: OnboardingStorageAdapter,
40
+ key: string = DEFAULT_KEY
41
+ ): Promise<void> {
42
+ await storage.removeItem(key);
43
+ }
44
+
45
+ /**
46
+ * Convenience factory that binds a storage adapter (and optional key) once and
47
+ * returns ready-to-call helpers, so app code doesn't repeat the storage arg.
48
+ */
49
+ export function createOnboardingStorage(
50
+ storage: OnboardingStorageAdapter,
51
+ key: string = DEFAULT_KEY
52
+ ) {
53
+ return {
54
+ hasCompleted: () => hasCompletedOnboarding(storage, key),
55
+ markComplete: () => markOnboardingComplete(storage, key),
56
+ reset: () => resetOnboarding(storage, key),
57
+ };
58
+ }