rn-onboarding-analytics 1.0.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/LICENSE +22 -0
- package/README.md +752 -0
- package/lib/module/index.js +26 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/spill-onboarding/adapters/expo-image.js +13 -0
- package/lib/module/spill-onboarding/adapters/expo-image.js.map +1 -0
- package/lib/module/spill-onboarding/adapters/react-native-svg.js +16 -0
- package/lib/module/spill-onboarding/adapters/react-native-svg.js.map +1 -0
- package/lib/module/spill-onboarding/analytics.js +56 -0
- package/lib/module/spill-onboarding/analytics.js.map +1 -0
- package/lib/module/spill-onboarding/buttons/PrimaryButton.js +50 -0
- package/lib/module/spill-onboarding/buttons/PrimaryButton.js.map +1 -0
- package/lib/module/spill-onboarding/buttons/SecondaryButton.js +51 -0
- package/lib/module/spill-onboarding/buttons/SecondaryButton.js.map +1 -0
- package/lib/module/spill-onboarding/buttons/SkipButton.js +35 -0
- package/lib/module/spill-onboarding/buttons/SkipButton.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingImageContainer.js +128 -0
- package/lib/module/spill-onboarding/components/OnboardingImageContainer.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingIntroPanel.js +97 -0
- package/lib/module/spill-onboarding/components/OnboardingIntroPanel.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingModal.js +69 -0
- package/lib/module/spill-onboarding/components/OnboardingModal.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingStepContainer.js +60 -0
- package/lib/module/spill-onboarding/components/OnboardingStepContainer.js.map +1 -0
- package/lib/module/spill-onboarding/components/OnboardingStepPanel.js +122 -0
- package/lib/module/spill-onboarding/components/OnboardingStepPanel.js.map +1 -0
- package/lib/module/spill-onboarding/hooks/useMeasureHeight.js +18 -0
- package/lib/module/spill-onboarding/hooks/useMeasureHeight.js.map +1 -0
- package/lib/module/spill-onboarding/icons/ArrowLeftIcon.js +57 -0
- package/lib/module/spill-onboarding/icons/ArrowLeftIcon.js.map +1 -0
- package/lib/module/spill-onboarding/icons/CloseIcon.js +49 -0
- package/lib/module/spill-onboarding/icons/CloseIcon.js.map +1 -0
- package/lib/module/spill-onboarding/index.js +206 -0
- package/lib/module/spill-onboarding/index.js.map +1 -0
- package/lib/module/spill-onboarding/types.js +4 -0
- package/lib/module/spill-onboarding/types.js.map +1 -0
- package/lib/module/utils/ThemeContext.js +78 -0
- package/lib/module/utils/ThemeContext.js.map +1 -0
- package/lib/module/utils/fontStyles.js +21 -0
- package/lib/module/utils/fontStyles.js.map +1 -0
- package/lib/module/utils/theme.js +27 -0
- package/lib/module/utils/theme.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/adapters/expo-image.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/adapters/expo-image.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/adapters/react-native-svg.d.ts +5 -0
- package/lib/typescript/src/spill-onboarding/adapters/react-native-svg.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/analytics.d.ts +2 -0
- package/lib/typescript/src/spill-onboarding/analytics.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/buttons/PrimaryButton.d.ts +13 -0
- package/lib/typescript/src/spill-onboarding/buttons/PrimaryButton.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/buttons/SecondaryButton.d.ts +13 -0
- package/lib/typescript/src/spill-onboarding/buttons/SecondaryButton.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/buttons/SkipButton.d.ts +6 -0
- package/lib/typescript/src/spill-onboarding/buttons/SkipButton.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingImageContainer.d.ts +18 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingImageContainer.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingIntroPanel.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingIntroPanel.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingModal.d.ts +8 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingModal.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepContainer.d.ts +16 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepContainer.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepPanel.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/components/OnboardingStepPanel.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/hooks/useMeasureHeight.d.ts +9 -0
- package/lib/typescript/src/spill-onboarding/hooks/useMeasureHeight.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/icons/ArrowLeftIcon.d.ts +7 -0
- package/lib/typescript/src/spill-onboarding/icons/ArrowLeftIcon.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/icons/CloseIcon.d.ts +7 -0
- package/lib/typescript/src/spill-onboarding/icons/CloseIcon.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/index.d.ts +4 -0
- package/lib/typescript/src/spill-onboarding/index.d.ts.map +1 -0
- package/lib/typescript/src/spill-onboarding/types.d.ts +192 -0
- package/lib/typescript/src/spill-onboarding/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/ThemeContext.d.ts +14 -0
- package/lib/typescript/src/utils/ThemeContext.d.ts.map +1 -0
- package/lib/typescript/src/utils/fontStyles.d.ts +19 -0
- package/lib/typescript/src/utils/fontStyles.d.ts.map +1 -0
- package/lib/typescript/src/utils/theme.d.ts +30 -0
- package/lib/typescript/src/utils/theme.d.ts.map +1 -0
- package/package.json +177 -0
- package/src/index.tsx +35 -0
- package/src/spill-onboarding/adapters/expo-image.ts +12 -0
- package/src/spill-onboarding/adapters/react-native-svg.ts +17 -0
- package/src/spill-onboarding/analytics.ts +75 -0
- package/src/spill-onboarding/buttons/PrimaryButton.tsx +70 -0
- package/src/spill-onboarding/buttons/SecondaryButton.tsx +71 -0
- package/src/spill-onboarding/buttons/SkipButton.tsx +34 -0
- package/src/spill-onboarding/components/OnboardingImageContainer.tsx +181 -0
- package/src/spill-onboarding/components/OnboardingIntroPanel.tsx +105 -0
- package/src/spill-onboarding/components/OnboardingModal.tsx +75 -0
- package/src/spill-onboarding/components/OnboardingStepContainer.tsx +85 -0
- package/src/spill-onboarding/components/OnboardingStepPanel.tsx +118 -0
- package/src/spill-onboarding/hooks/useMeasureHeight.ts +21 -0
- package/src/spill-onboarding/icons/ArrowLeftIcon.tsx +69 -0
- package/src/spill-onboarding/icons/CloseIcon.tsx +55 -0
- package/src/spill-onboarding/index.tsx +251 -0
- package/src/spill-onboarding/types.ts +243 -0
- package/src/utils/ThemeContext.tsx +87 -0
- package/src/utils/fontStyles.ts +19 -0
- package/src/utils/theme.ts +29 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { useMemo, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type ImageSourcePropType,
|
|
4
|
+
View,
|
|
5
|
+
BackHandler,
|
|
6
|
+
Platform,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { useTheme } from '../utils/ThemeContext';
|
|
10
|
+
import OnboardingIntroPanel from './components/OnboardingIntroPanel';
|
|
11
|
+
import { useSharedValue, withTiming } from 'react-native-reanimated';
|
|
12
|
+
import OnboardingStepPanel from './components/OnboardingStepPanel';
|
|
13
|
+
import OnboardingStepContainer from './components/OnboardingStepContainer';
|
|
14
|
+
import OnboardingImageContainer from './components/OnboardingImageContainer';
|
|
15
|
+
import OnboardingModal from './components/OnboardingModal';
|
|
16
|
+
import { type OnboardingProps } from './types';
|
|
17
|
+
import useMeasureHeight from './hooks/useMeasureHeight';
|
|
18
|
+
import { type Theme } from '../utils/theme';
|
|
19
|
+
import { trackEvent } from './analytics';
|
|
20
|
+
|
|
21
|
+
function SpillOnboarding({
|
|
22
|
+
animationDuration = 500,
|
|
23
|
+
introPanel: introPanelProps,
|
|
24
|
+
steps,
|
|
25
|
+
onComplete,
|
|
26
|
+
onSkip,
|
|
27
|
+
onStepChange: onStepChangeProps,
|
|
28
|
+
showCloseButton = true,
|
|
29
|
+
showBackButton = true,
|
|
30
|
+
wrapInModalOnWeb = true,
|
|
31
|
+
background,
|
|
32
|
+
skipButton,
|
|
33
|
+
apiKey,
|
|
34
|
+
}: OnboardingProps) {
|
|
35
|
+
const { theme } = useTheme();
|
|
36
|
+
|
|
37
|
+
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
38
|
+
const backgroundSpillProgress = useSharedValue(0);
|
|
39
|
+
|
|
40
|
+
const [step, setStep] = useState(-1);
|
|
41
|
+
const currentStep = step >= 0 ? steps[step] : undefined;
|
|
42
|
+
const firstStep = steps[0];
|
|
43
|
+
|
|
44
|
+
const onStepChange = useCallback(
|
|
45
|
+
(stepNumber: number) => {
|
|
46
|
+
const getStepName = (index: number) => {
|
|
47
|
+
if (index === -1) {
|
|
48
|
+
return 'Intro';
|
|
49
|
+
}
|
|
50
|
+
const s = steps[index];
|
|
51
|
+
if (!s) {
|
|
52
|
+
return 'Unknown';
|
|
53
|
+
}
|
|
54
|
+
if ('title' in s && s.title) {
|
|
55
|
+
return s.title;
|
|
56
|
+
}
|
|
57
|
+
return `Step ${index + 1}`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const fromStepName = getStepName(step);
|
|
61
|
+
const toStepName = getStepName(stepNumber);
|
|
62
|
+
|
|
63
|
+
console.log('🔄 Onboarding Step Change:', {
|
|
64
|
+
from: fromStepName,
|
|
65
|
+
to: toStepName,
|
|
66
|
+
index: stepNumber,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
setStep(stepNumber);
|
|
70
|
+
onStepChangeProps?.(stepNumber);
|
|
71
|
+
|
|
72
|
+
trackEvent(apiKey, 'step_change', {
|
|
73
|
+
from_index: step,
|
|
74
|
+
to_index: stepNumber,
|
|
75
|
+
from_step: fromStepName,
|
|
76
|
+
to_step: toStepName,
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
[onStepChangeProps, apiKey, step, steps]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const backHandler = BackHandler.addEventListener(
|
|
84
|
+
'hardwareBackPress',
|
|
85
|
+
() => {
|
|
86
|
+
if (step > 0) {
|
|
87
|
+
onStepChange(step - 1);
|
|
88
|
+
return true;
|
|
89
|
+
} else if (step === 0) {
|
|
90
|
+
backgroundSpillProgress.set(
|
|
91
|
+
withTiming(0, {
|
|
92
|
+
duration: animationDuration,
|
|
93
|
+
})
|
|
94
|
+
);
|
|
95
|
+
setTimeout(() => setStep(-1), animationDuration / 2);
|
|
96
|
+
onStepChange(-1);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// stepNumber === -1 (intro panel) - allow default back action
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return () => backHandler.remove();
|
|
106
|
+
}, [step, backgroundSpillProgress, onStepChange, animationDuration]);
|
|
107
|
+
|
|
108
|
+
const introPanel = useMeasureHeight();
|
|
109
|
+
const stepPanel = useMeasureHeight();
|
|
110
|
+
const screen = useMeasureHeight();
|
|
111
|
+
|
|
112
|
+
const onPressStart = () => {
|
|
113
|
+
backgroundSpillProgress.set(
|
|
114
|
+
withTiming(1, {
|
|
115
|
+
duration: animationDuration,
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
onStepChange(0);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const onNextPress = () => {
|
|
122
|
+
if (step === steps.length - 1) {
|
|
123
|
+
trackEvent(apiKey, 'complete');
|
|
124
|
+
return onComplete();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
onStepChange(step + 1);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const onBackPress = () => {
|
|
131
|
+
if (step === 0) {
|
|
132
|
+
backgroundSpillProgress.set(
|
|
133
|
+
withTiming(0, {
|
|
134
|
+
duration: animationDuration,
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
onStepChange(-1);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onStepChange(step - 1);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const renderIntroPanel = () => {
|
|
146
|
+
if (typeof introPanelProps === 'function') {
|
|
147
|
+
return introPanelProps({ onPressStart });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<OnboardingIntroPanel
|
|
152
|
+
onPressStart={onPressStart}
|
|
153
|
+
title={introPanelProps.title}
|
|
154
|
+
subtitle={introPanelProps.subtitle}
|
|
155
|
+
button={introPanelProps.button}
|
|
156
|
+
image={introPanelProps.image}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const renderStepContent = () => {
|
|
162
|
+
if (!currentStep) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (typeof currentStep.component === 'function') {
|
|
166
|
+
return currentStep.component({
|
|
167
|
+
onNext: onNextPress,
|
|
168
|
+
onBack: onBackPress,
|
|
169
|
+
isLast: step === steps.length - 1,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<OnboardingStepPanel
|
|
175
|
+
label={currentStep.label}
|
|
176
|
+
title={currentStep.title}
|
|
177
|
+
description={currentStep.description}
|
|
178
|
+
buttonLabel={currentStep.buttonLabel}
|
|
179
|
+
onBackPress={onBackPress}
|
|
180
|
+
onNextPress={onNextPress}
|
|
181
|
+
buttonPrimary={step === steps.length - 1}
|
|
182
|
+
showBackButton={showBackButton}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const currentStepImage: ImageSourcePropType | undefined = useMemo(() => {
|
|
188
|
+
if (!currentStep) {
|
|
189
|
+
return firstStep?.image;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return currentStep.image;
|
|
193
|
+
}, [currentStep, firstStep?.image]);
|
|
194
|
+
|
|
195
|
+
const onboardingContent = (
|
|
196
|
+
<View style={styles.container} ref={screen.ref}>
|
|
197
|
+
<View ref={introPanel.ref} style={styles.bottomPanel}>
|
|
198
|
+
{renderIntroPanel()}
|
|
199
|
+
</View>
|
|
200
|
+
|
|
201
|
+
<OnboardingImageContainer
|
|
202
|
+
currentStep={currentStep}
|
|
203
|
+
currentStepImage={currentStepImage}
|
|
204
|
+
position={currentStep?.position ?? firstStep?.position ?? 'top'}
|
|
205
|
+
animationDuration={animationDuration}
|
|
206
|
+
backgroundSpillProgress={backgroundSpillProgress}
|
|
207
|
+
screenHeight={screen.height}
|
|
208
|
+
introPanel={introPanel}
|
|
209
|
+
stepPanel={stepPanel}
|
|
210
|
+
background={background}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
<OnboardingStepContainer
|
|
214
|
+
currentStep={currentStep}
|
|
215
|
+
animationDuration={animationDuration}
|
|
216
|
+
showCloseButton={showCloseButton}
|
|
217
|
+
renderStepContent={renderStepContent}
|
|
218
|
+
onSkip={onSkip}
|
|
219
|
+
ref={stepPanel.ref}
|
|
220
|
+
skipButton={skipButton}
|
|
221
|
+
/>
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// On web, wrap in modal; on mobile, render directly
|
|
226
|
+
if (Platform.OS === 'web' && wrapInModalOnWeb) {
|
|
227
|
+
return (
|
|
228
|
+
<OnboardingModal onSkip={onSkip}>{onboardingContent}</OnboardingModal>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return onboardingContent;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export default SpillOnboarding;
|
|
236
|
+
|
|
237
|
+
const createStyles = (theme: Theme) =>
|
|
238
|
+
StyleSheet.create({
|
|
239
|
+
container: {
|
|
240
|
+
flex: 1,
|
|
241
|
+
backgroundColor: theme.bg.secondary,
|
|
242
|
+
},
|
|
243
|
+
bottomPanel: {
|
|
244
|
+
paddingHorizontal: 16,
|
|
245
|
+
paddingBottom: 16 + theme.insets.bottom,
|
|
246
|
+
position: 'absolute',
|
|
247
|
+
bottom: 0,
|
|
248
|
+
left: 0,
|
|
249
|
+
right: 0,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { ImageSourcePropType } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Theme color tokens used by onboarding components.
|
|
6
|
+
*/
|
|
7
|
+
export interface OnboardingColors {
|
|
8
|
+
/**
|
|
9
|
+
* Background colors used across the UI.
|
|
10
|
+
*/
|
|
11
|
+
background: {
|
|
12
|
+
/**
|
|
13
|
+
* Primary page/screen background color.
|
|
14
|
+
*/
|
|
15
|
+
primary: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Background for panels or cards.
|
|
19
|
+
*/
|
|
20
|
+
secondary: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Subtle background for labels.
|
|
24
|
+
*/
|
|
25
|
+
label: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Accented background for emphasis areas.
|
|
29
|
+
*/
|
|
30
|
+
accent: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Text colors used for typography.
|
|
35
|
+
*/
|
|
36
|
+
text: {
|
|
37
|
+
/**
|
|
38
|
+
* Default body text color for readability on `background.primary`.
|
|
39
|
+
*/
|
|
40
|
+
primary: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Subdued text color for secondary information.
|
|
44
|
+
*/
|
|
45
|
+
secondary: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* High-contrast text color intended for buttons/overlays.
|
|
49
|
+
*/
|
|
50
|
+
contrast: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Font family names used for specific text roles. Values are platform-registered font family strings.
|
|
56
|
+
*/
|
|
57
|
+
export interface OnboardingFonts {
|
|
58
|
+
/** Font for the intro screen title. */
|
|
59
|
+
introTitle?: string;
|
|
60
|
+
|
|
61
|
+
/** Font for the intro screen subtitle. */
|
|
62
|
+
introSubtitle?: string;
|
|
63
|
+
|
|
64
|
+
/** Font for the intro screen button label. */
|
|
65
|
+
introButton?: string;
|
|
66
|
+
|
|
67
|
+
/** Font for a step label (small caption above title). */
|
|
68
|
+
stepLabel?: string;
|
|
69
|
+
|
|
70
|
+
/** Font for a step title. */
|
|
71
|
+
stepTitle?: string;
|
|
72
|
+
|
|
73
|
+
/** Font for a step description/body. */
|
|
74
|
+
stepDescription?: string;
|
|
75
|
+
|
|
76
|
+
/** Font for a step primary action label. */
|
|
77
|
+
stepButton?: string;
|
|
78
|
+
|
|
79
|
+
/** Font for primary button labels. */
|
|
80
|
+
primaryButton?: string;
|
|
81
|
+
|
|
82
|
+
/** Font for secondary button labels. */
|
|
83
|
+
secondaryButton?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Props for the introductory panel shown before steps begin.
|
|
88
|
+
*/
|
|
89
|
+
export interface OnboardingIntroPanelProps {
|
|
90
|
+
/** Callback invoked when the user starts the onboarding. */
|
|
91
|
+
onPressStart: () => void;
|
|
92
|
+
|
|
93
|
+
/** Title content; string or custom React node. */
|
|
94
|
+
title?: string | ReactNode;
|
|
95
|
+
|
|
96
|
+
/** Subtitle content; string or custom React node. */
|
|
97
|
+
subtitle?: string | ReactNode;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Button content. Either a simple string label or a render function
|
|
101
|
+
* that receives `onPressStart` to wire up a custom button.
|
|
102
|
+
*/
|
|
103
|
+
button:
|
|
104
|
+
| string
|
|
105
|
+
| (({ onPressStart }: { onPressStart: () => void }) => ReactNode);
|
|
106
|
+
|
|
107
|
+
/** Optional image shown on the intro panel. */
|
|
108
|
+
image?: ImageSourcePropType | (() => ReactNode);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type OnboardingStepDefault = {
|
|
112
|
+
/** Discriminator: for default steps, `component` must be omitted. */
|
|
113
|
+
component?: never;
|
|
114
|
+
|
|
115
|
+
/** Optional small label displayed above the title. */
|
|
116
|
+
label?: string;
|
|
117
|
+
|
|
118
|
+
/** Step title text. */
|
|
119
|
+
title: string;
|
|
120
|
+
|
|
121
|
+
/** Step description/body text. */
|
|
122
|
+
description: string;
|
|
123
|
+
|
|
124
|
+
/** Label for the primary action button. */
|
|
125
|
+
buttonLabel: string;
|
|
126
|
+
|
|
127
|
+
/** Image displayed alongside the step content. */
|
|
128
|
+
image: ImageSourcePropType;
|
|
129
|
+
|
|
130
|
+
/** Placement of the image relative to content. */
|
|
131
|
+
position: 'top' | 'bottom';
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
type OnboardingStepCustom = {
|
|
135
|
+
/**
|
|
136
|
+
* Custom step renderer. Receives navigation helpers and state.
|
|
137
|
+
*/
|
|
138
|
+
component: (props: {
|
|
139
|
+
/** Advance to the next step. */
|
|
140
|
+
onNext: () => void;
|
|
141
|
+
/** Go back to the previous step. */
|
|
142
|
+
onBack: () => void;
|
|
143
|
+
/** True if this is the last step. */
|
|
144
|
+
isLast: boolean;
|
|
145
|
+
}) => ReactNode;
|
|
146
|
+
|
|
147
|
+
/** Image displayed alongside the custom step. */
|
|
148
|
+
image: ImageSourcePropType;
|
|
149
|
+
|
|
150
|
+
/** Placement of the image relative to content. */
|
|
151
|
+
position: 'top' | 'bottom';
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A single onboarding step. Either a default text-based step or a fully custom component.
|
|
156
|
+
*/
|
|
157
|
+
export type OnboardingStep = OnboardingStepDefault | OnboardingStepCustom;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Props consumed by the internal step panel component.
|
|
161
|
+
*/
|
|
162
|
+
export interface OnboardingStepPanelProps {
|
|
163
|
+
/** Optional small label displayed above the title. */
|
|
164
|
+
label?: string;
|
|
165
|
+
|
|
166
|
+
/** Step title text. */
|
|
167
|
+
title: string;
|
|
168
|
+
|
|
169
|
+
/** Step description text. */
|
|
170
|
+
description: string;
|
|
171
|
+
|
|
172
|
+
/** Label for the primary action button. */
|
|
173
|
+
buttonLabel: string;
|
|
174
|
+
|
|
175
|
+
/** Handler for the back button. */
|
|
176
|
+
onBackPress?: () => void;
|
|
177
|
+
|
|
178
|
+
/** Handler for the next/continue button. */
|
|
179
|
+
onNextPress: () => void;
|
|
180
|
+
|
|
181
|
+
/** Whether the primary styling should be applied to the button. */
|
|
182
|
+
buttonPrimary: boolean;
|
|
183
|
+
|
|
184
|
+
/** Controls visibility of the back button. */
|
|
185
|
+
showBackButton?: boolean;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type OnboardingIntroPanel =
|
|
189
|
+
| Omit<OnboardingIntroPanelProps, 'onPressStart'>
|
|
190
|
+
| (({ onPressStart }: { onPressStart: () => void }) => ReactNode);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Top-level props for the `Onboarding` component.
|
|
194
|
+
*/
|
|
195
|
+
export interface OnboardingProps {
|
|
196
|
+
/** Duration in milliseconds for step transition animations. */
|
|
197
|
+
animationDuration?: number;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Intro panel content. Either props for the default panel (without `onPressStart`)
|
|
201
|
+
* or a render function receiving `onPressStart` for a fully custom intro.
|
|
202
|
+
*/
|
|
203
|
+
introPanel: OnboardingIntroPanel;
|
|
204
|
+
|
|
205
|
+
/** Ordered list of steps to render. */
|
|
206
|
+
steps: OnboardingStep[];
|
|
207
|
+
|
|
208
|
+
/** Called when the user completes the final step. */
|
|
209
|
+
onComplete: () => void;
|
|
210
|
+
|
|
211
|
+
/** Called when the user skips the onboarding. */
|
|
212
|
+
onSkip?: () => void;
|
|
213
|
+
|
|
214
|
+
/** Notifies consumers when the active step index changes. */
|
|
215
|
+
onStepChange?: (stepIndex: number) => void;
|
|
216
|
+
|
|
217
|
+
/** Whether to show the close button in the header. */
|
|
218
|
+
showCloseButton?: boolean;
|
|
219
|
+
|
|
220
|
+
/** Whether to show a back button on steps */
|
|
221
|
+
showBackButton?: boolean;
|
|
222
|
+
|
|
223
|
+
/** Whether to wrap the onboarding in a modal on web. */
|
|
224
|
+
wrapInModalOnWeb?: boolean;
|
|
225
|
+
|
|
226
|
+
/** Optional custom background element rendered behind content. */
|
|
227
|
+
background?: () => ReactNode;
|
|
228
|
+
|
|
229
|
+
/** Optional custom close button renderer. */
|
|
230
|
+
skipButton?: ({ onPress }: { onPress: () => void }) => ReactNode;
|
|
231
|
+
|
|
232
|
+
/** Theme colors to use for styling. */
|
|
233
|
+
colors?: OnboardingColors;
|
|
234
|
+
|
|
235
|
+
/** Font family set or a single family name applied where appropriate. */
|
|
236
|
+
fonts?: OnboardingFonts | string;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* API Key for analytics.
|
|
240
|
+
* Required to enable analytics tracking.
|
|
241
|
+
*/
|
|
242
|
+
apiKey: string;
|
|
243
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { defaultTheme, type Theme } from './theme';
|
|
3
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
4
|
+
import {
|
|
5
|
+
type OnboardingColors,
|
|
6
|
+
type OnboardingFonts,
|
|
7
|
+
} from '../spill-onboarding/types';
|
|
8
|
+
|
|
9
|
+
const ThemeContext = createContext<{ theme: Theme }>({
|
|
10
|
+
theme: {
|
|
11
|
+
...defaultTheme,
|
|
12
|
+
insets: { top: 0, bottom: 0, left: 0, right: 0 },
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
interface ThemeProviderProps {
|
|
17
|
+
children: React.ReactNode;
|
|
18
|
+
colors?: OnboardingColors;
|
|
19
|
+
fonts?: OnboardingFonts | string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function ThemeProvider({
|
|
23
|
+
children,
|
|
24
|
+
colors: customColors,
|
|
25
|
+
fonts: customFonts,
|
|
26
|
+
}: ThemeProviderProps) {
|
|
27
|
+
const insets = useSafeAreaInsets();
|
|
28
|
+
|
|
29
|
+
const theme: Theme = useMemo(() => {
|
|
30
|
+
const fonts =
|
|
31
|
+
typeof customFonts === 'string'
|
|
32
|
+
? {
|
|
33
|
+
introTitle: customFonts,
|
|
34
|
+
introSubtitle: customFonts,
|
|
35
|
+
introButton: customFonts,
|
|
36
|
+
stepLabel: customFonts,
|
|
37
|
+
stepTitle: customFonts,
|
|
38
|
+
stepDescription: customFonts,
|
|
39
|
+
stepButton: customFonts,
|
|
40
|
+
primaryButton: customFonts,
|
|
41
|
+
secondaryButton: customFonts,
|
|
42
|
+
}
|
|
43
|
+
: {
|
|
44
|
+
introTitle:
|
|
45
|
+
customFonts?.introTitle ?? defaultTheme.fonts.introTitle,
|
|
46
|
+
introSubtitle:
|
|
47
|
+
customFonts?.introSubtitle ?? defaultTheme.fonts.introSubtitle,
|
|
48
|
+
introButton:
|
|
49
|
+
customFonts?.introButton ?? defaultTheme.fonts.introButton,
|
|
50
|
+
stepLabel: customFonts?.stepLabel ?? defaultTheme.fonts.stepLabel,
|
|
51
|
+
stepTitle: customFonts?.stepTitle ?? defaultTheme.fonts.stepTitle,
|
|
52
|
+
stepDescription:
|
|
53
|
+
customFonts?.stepDescription ??
|
|
54
|
+
defaultTheme.fonts.stepDescription,
|
|
55
|
+
stepButton:
|
|
56
|
+
customFonts?.stepButton ?? defaultTheme.fonts.stepButton,
|
|
57
|
+
primaryButton:
|
|
58
|
+
customFonts?.primaryButton ?? defaultTheme.fonts.primaryButton,
|
|
59
|
+
secondaryButton:
|
|
60
|
+
customFonts?.secondaryButton ??
|
|
61
|
+
defaultTheme.fonts.secondaryButton,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const { background, text: textColors } = customColors ?? {};
|
|
65
|
+
const bg = {
|
|
66
|
+
primary: background?.primary ?? defaultTheme.bg.primary,
|
|
67
|
+
secondary: background?.secondary ?? defaultTheme.bg.secondary,
|
|
68
|
+
label: background?.label ?? defaultTheme.bg.label,
|
|
69
|
+
accent: background?.accent ?? defaultTheme.bg.accent,
|
|
70
|
+
};
|
|
71
|
+
const text = {
|
|
72
|
+
primary: textColors?.primary ?? defaultTheme.text.primary,
|
|
73
|
+
secondary: textColors?.secondary ?? defaultTheme.text.secondary,
|
|
74
|
+
contrast: textColors?.contrast ?? defaultTheme.text.contrast,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return { bg, text, fonts, insets };
|
|
78
|
+
}, [insets, customColors, customFonts]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<ThemeContext.Provider value={{ theme }}>{children}</ThemeContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useTheme() {
|
|
86
|
+
return useContext(ThemeContext);
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EdgeInsets } from 'react-native-safe-area-context';
|
|
2
|
+
|
|
3
|
+
export const defaultTheme = {
|
|
4
|
+
bg: {
|
|
5
|
+
primary: '#007AFF',
|
|
6
|
+
secondary: '#FFFFFF',
|
|
7
|
+
label: '#F2F2F7',
|
|
8
|
+
accent: '#1C1C1E',
|
|
9
|
+
},
|
|
10
|
+
text: {
|
|
11
|
+
primary: '#1C1C1E',
|
|
12
|
+
secondary: '#8E8E93',
|
|
13
|
+
contrast: '#FFFFFF',
|
|
14
|
+
},
|
|
15
|
+
fonts: {
|
|
16
|
+
introTitle: 'System',
|
|
17
|
+
introSubtitle: 'System',
|
|
18
|
+
introButton: 'System',
|
|
19
|
+
stepLabel: 'System',
|
|
20
|
+
stepTitle: 'System',
|
|
21
|
+
stepDescription: 'System',
|
|
22
|
+
stepButton: 'System',
|
|
23
|
+
primaryButton: 'System',
|
|
24
|
+
secondaryButton: 'System',
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ThemeColors = typeof defaultTheme;
|
|
29
|
+
export type Theme = ThemeColors & { insets: EdgeInsets };
|