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.
- package/README.md +89 -7
- package/lib/commonjs/components/CustomPages.js +31 -55
- package/lib/commonjs/components/CustomPages.js.map +1 -1
- package/lib/commonjs/components/OnboardingPages.js +59 -74
- package/lib/commonjs/components/OnboardingPages.js.map +1 -1
- package/lib/commonjs/components/Page.js +8 -3
- package/lib/commonjs/components/Page.js.map +1 -1
- package/lib/commonjs/components/Pagination.js +75 -13
- package/lib/commonjs/components/Pagination.js.map +1 -1
- package/lib/commonjs/components/Swiper.js +58 -85
- package/lib/commonjs/components/Swiper.js.map +1 -1
- package/lib/commonjs/components/button.js +3 -1
- package/lib/commonjs/components/button.js.map +1 -1
- package/lib/commonjs/context/OnboardingContext.js +101 -21
- package/lib/commonjs/context/OnboardingContext.js.map +1 -1
- package/lib/commonjs/hooks/useOnboarding.js +1 -1
- package/lib/commonjs/hooks/useOnboarding.js.map +1 -1
- package/lib/commonjs/index.js +33 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/utils/persistence.js +51 -0
- package/lib/commonjs/utils/persistence.js.map +1 -0
- package/lib/module/components/CustomPages.js +31 -55
- package/lib/module/components/CustomPages.js.map +1 -1
- package/lib/module/components/OnboardingPages.js +60 -75
- package/lib/module/components/OnboardingPages.js.map +1 -1
- package/lib/module/components/Page.js +8 -3
- package/lib/module/components/Page.js.map +1 -1
- package/lib/module/components/Pagination.js +76 -14
- package/lib/module/components/Pagination.js.map +1 -1
- package/lib/module/components/Swiper.js +59 -86
- package/lib/module/components/Swiper.js.map +1 -1
- package/lib/module/components/button.js +3 -1
- package/lib/module/components/button.js.map +1 -1
- package/lib/module/context/OnboardingContext.js +102 -22
- package/lib/module/context/OnboardingContext.js.map +1 -1
- package/lib/module/hooks/useOnboarding.js +1 -1
- package/lib/module/hooks/useOnboarding.js.map +1 -1
- package/lib/module/index.js +8 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/persistence.js +42 -0
- package/lib/module/utils/persistence.js.map +1 -0
- package/lib/typescript/src/components/CustomPages.d.ts +6 -2
- package/lib/typescript/src/components/CustomPages.d.ts.map +1 -1
- package/lib/typescript/src/components/OnboardingPages.d.ts +6 -2
- package/lib/typescript/src/components/OnboardingPages.d.ts.map +1 -1
- package/lib/typescript/src/components/Page.d.ts +2 -0
- package/lib/typescript/src/components/Page.d.ts.map +1 -1
- package/lib/typescript/src/components/Pagination.d.ts +9 -0
- package/lib/typescript/src/components/Pagination.d.ts.map +1 -1
- package/lib/typescript/src/components/Swiper.d.ts.map +1 -1
- package/lib/typescript/src/components/button.d.ts.map +1 -1
- package/lib/typescript/src/context/OnboardingContext.d.ts +9 -0
- package/lib/typescript/src/context/OnboardingContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useOnboarding.d.ts +3 -0
- package/lib/typescript/src/hooks/useOnboarding.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/index.d.ts +13 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/persistence.d.ts +31 -0
- package/lib/typescript/src/utils/persistence.d.ts.map +1 -0
- package/package.json +13 -3
- package/src/components/CustomPages.tsx +62 -69
- package/src/components/OnboardingPages.tsx +79 -82
- package/src/components/Page.tsx +8 -2
- package/src/components/Pagination.tsx +121 -29
- package/src/components/Swiper.tsx +65 -87
- package/src/components/button.tsx +6 -1
- package/src/context/OnboardingContext.tsx +145 -26
- package/src/hooks/useOnboarding.tsx +1 -3
- package/src/index.tsx +16 -1
- package/src/types/index.ts +13 -0
- package/src/utils/persistence.ts +58 -0
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
flatListRef.current
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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>
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
+
}
|