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.
Files changed (105) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +752 -0
  3. package/lib/module/index.js +26 -0
  4. package/lib/module/index.js.map +1 -0
  5. package/lib/module/package.json +1 -0
  6. package/lib/module/spill-onboarding/adapters/expo-image.js +13 -0
  7. package/lib/module/spill-onboarding/adapters/expo-image.js.map +1 -0
  8. package/lib/module/spill-onboarding/adapters/react-native-svg.js +16 -0
  9. package/lib/module/spill-onboarding/adapters/react-native-svg.js.map +1 -0
  10. package/lib/module/spill-onboarding/analytics.js +56 -0
  11. package/lib/module/spill-onboarding/analytics.js.map +1 -0
  12. package/lib/module/spill-onboarding/buttons/PrimaryButton.js +50 -0
  13. package/lib/module/spill-onboarding/buttons/PrimaryButton.js.map +1 -0
  14. package/lib/module/spill-onboarding/buttons/SecondaryButton.js +51 -0
  15. package/lib/module/spill-onboarding/buttons/SecondaryButton.js.map +1 -0
  16. package/lib/module/spill-onboarding/buttons/SkipButton.js +35 -0
  17. package/lib/module/spill-onboarding/buttons/SkipButton.js.map +1 -0
  18. package/lib/module/spill-onboarding/components/OnboardingImageContainer.js +128 -0
  19. package/lib/module/spill-onboarding/components/OnboardingImageContainer.js.map +1 -0
  20. package/lib/module/spill-onboarding/components/OnboardingIntroPanel.js +97 -0
  21. package/lib/module/spill-onboarding/components/OnboardingIntroPanel.js.map +1 -0
  22. package/lib/module/spill-onboarding/components/OnboardingModal.js +69 -0
  23. package/lib/module/spill-onboarding/components/OnboardingModal.js.map +1 -0
  24. package/lib/module/spill-onboarding/components/OnboardingStepContainer.js +60 -0
  25. package/lib/module/spill-onboarding/components/OnboardingStepContainer.js.map +1 -0
  26. package/lib/module/spill-onboarding/components/OnboardingStepPanel.js +122 -0
  27. package/lib/module/spill-onboarding/components/OnboardingStepPanel.js.map +1 -0
  28. package/lib/module/spill-onboarding/hooks/useMeasureHeight.js +18 -0
  29. package/lib/module/spill-onboarding/hooks/useMeasureHeight.js.map +1 -0
  30. package/lib/module/spill-onboarding/icons/ArrowLeftIcon.js +57 -0
  31. package/lib/module/spill-onboarding/icons/ArrowLeftIcon.js.map +1 -0
  32. package/lib/module/spill-onboarding/icons/CloseIcon.js +49 -0
  33. package/lib/module/spill-onboarding/icons/CloseIcon.js.map +1 -0
  34. package/lib/module/spill-onboarding/index.js +206 -0
  35. package/lib/module/spill-onboarding/index.js.map +1 -0
  36. package/lib/module/spill-onboarding/types.js +4 -0
  37. package/lib/module/spill-onboarding/types.js.map +1 -0
  38. package/lib/module/utils/ThemeContext.js +78 -0
  39. package/lib/module/utils/ThemeContext.js.map +1 -0
  40. package/lib/module/utils/fontStyles.js +21 -0
  41. package/lib/module/utils/fontStyles.js.map +1 -0
  42. package/lib/module/utils/theme.js +27 -0
  43. package/lib/module/utils/theme.js.map +1 -0
  44. package/lib/typescript/package.json +1 -0
  45. package/lib/typescript/src/index.d.ts +5 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/lib/typescript/src/spill-onboarding/adapters/expo-image.d.ts +4 -0
  48. package/lib/typescript/src/spill-onboarding/adapters/expo-image.d.ts.map +1 -0
  49. package/lib/typescript/src/spill-onboarding/adapters/react-native-svg.d.ts +5 -0
  50. package/lib/typescript/src/spill-onboarding/adapters/react-native-svg.d.ts.map +1 -0
  51. package/lib/typescript/src/spill-onboarding/analytics.d.ts +2 -0
  52. package/lib/typescript/src/spill-onboarding/analytics.d.ts.map +1 -0
  53. package/lib/typescript/src/spill-onboarding/buttons/PrimaryButton.d.ts +13 -0
  54. package/lib/typescript/src/spill-onboarding/buttons/PrimaryButton.d.ts.map +1 -0
  55. package/lib/typescript/src/spill-onboarding/buttons/SecondaryButton.d.ts +13 -0
  56. package/lib/typescript/src/spill-onboarding/buttons/SecondaryButton.d.ts.map +1 -0
  57. package/lib/typescript/src/spill-onboarding/buttons/SkipButton.d.ts +6 -0
  58. package/lib/typescript/src/spill-onboarding/buttons/SkipButton.d.ts.map +1 -0
  59. package/lib/typescript/src/spill-onboarding/components/OnboardingImageContainer.d.ts +18 -0
  60. package/lib/typescript/src/spill-onboarding/components/OnboardingImageContainer.d.ts.map +1 -0
  61. package/lib/typescript/src/spill-onboarding/components/OnboardingIntroPanel.d.ts +4 -0
  62. package/lib/typescript/src/spill-onboarding/components/OnboardingIntroPanel.d.ts.map +1 -0
  63. package/lib/typescript/src/spill-onboarding/components/OnboardingModal.d.ts +8 -0
  64. package/lib/typescript/src/spill-onboarding/components/OnboardingModal.d.ts.map +1 -0
  65. package/lib/typescript/src/spill-onboarding/components/OnboardingStepContainer.d.ts +16 -0
  66. package/lib/typescript/src/spill-onboarding/components/OnboardingStepContainer.d.ts.map +1 -0
  67. package/lib/typescript/src/spill-onboarding/components/OnboardingStepPanel.d.ts +4 -0
  68. package/lib/typescript/src/spill-onboarding/components/OnboardingStepPanel.d.ts.map +1 -0
  69. package/lib/typescript/src/spill-onboarding/hooks/useMeasureHeight.d.ts +9 -0
  70. package/lib/typescript/src/spill-onboarding/hooks/useMeasureHeight.d.ts.map +1 -0
  71. package/lib/typescript/src/spill-onboarding/icons/ArrowLeftIcon.d.ts +7 -0
  72. package/lib/typescript/src/spill-onboarding/icons/ArrowLeftIcon.d.ts.map +1 -0
  73. package/lib/typescript/src/spill-onboarding/icons/CloseIcon.d.ts +7 -0
  74. package/lib/typescript/src/spill-onboarding/icons/CloseIcon.d.ts.map +1 -0
  75. package/lib/typescript/src/spill-onboarding/index.d.ts +4 -0
  76. package/lib/typescript/src/spill-onboarding/index.d.ts.map +1 -0
  77. package/lib/typescript/src/spill-onboarding/types.d.ts +192 -0
  78. package/lib/typescript/src/spill-onboarding/types.d.ts.map +1 -0
  79. package/lib/typescript/src/utils/ThemeContext.d.ts +14 -0
  80. package/lib/typescript/src/utils/ThemeContext.d.ts.map +1 -0
  81. package/lib/typescript/src/utils/fontStyles.d.ts +19 -0
  82. package/lib/typescript/src/utils/fontStyles.d.ts.map +1 -0
  83. package/lib/typescript/src/utils/theme.d.ts +30 -0
  84. package/lib/typescript/src/utils/theme.d.ts.map +1 -0
  85. package/package.json +177 -0
  86. package/src/index.tsx +35 -0
  87. package/src/spill-onboarding/adapters/expo-image.ts +12 -0
  88. package/src/spill-onboarding/adapters/react-native-svg.ts +17 -0
  89. package/src/spill-onboarding/analytics.ts +75 -0
  90. package/src/spill-onboarding/buttons/PrimaryButton.tsx +70 -0
  91. package/src/spill-onboarding/buttons/SecondaryButton.tsx +71 -0
  92. package/src/spill-onboarding/buttons/SkipButton.tsx +34 -0
  93. package/src/spill-onboarding/components/OnboardingImageContainer.tsx +181 -0
  94. package/src/spill-onboarding/components/OnboardingIntroPanel.tsx +105 -0
  95. package/src/spill-onboarding/components/OnboardingModal.tsx +75 -0
  96. package/src/spill-onboarding/components/OnboardingStepContainer.tsx +85 -0
  97. package/src/spill-onboarding/components/OnboardingStepPanel.tsx +118 -0
  98. package/src/spill-onboarding/hooks/useMeasureHeight.ts +21 -0
  99. package/src/spill-onboarding/icons/ArrowLeftIcon.tsx +69 -0
  100. package/src/spill-onboarding/icons/CloseIcon.tsx +55 -0
  101. package/src/spill-onboarding/index.tsx +251 -0
  102. package/src/spill-onboarding/types.ts +243 -0
  103. package/src/utils/ThemeContext.tsx +87 -0
  104. package/src/utils/fontStyles.ts +19 -0
  105. package/src/utils/theme.ts +29 -0
package/package.json ADDED
@@ -0,0 +1,177 @@
1
+ {
2
+ "name": "rn-onboarding-analytics",
3
+ "version": "1.0.0",
4
+ "description": "Onboarding/tutorial flow for React Native with analytics.",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "test": "jest --passWithNoTests",
36
+ "typecheck": "tsc",
37
+ "lint:fix": "eslint \"**/*.{js,ts,tsx}\" --fix",
38
+ "clean": "del-cli lib",
39
+ "prepare": "bob build",
40
+ "prepack": "sed -i '' '1s|.*|![Banner](assets/banner.png)|' README.md",
41
+ "postpack": "git checkout README.md",
42
+ "release": "release-it --only-version"
43
+ },
44
+ "keywords": [
45
+ "react-native",
46
+ "ios",
47
+ "android",
48
+ "analytics"
49
+ ],
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/Ryan-Carloso/react-native-onboarding-w-analytics.git"
53
+ },
54
+ "author": "Ryan Carloso <carlosoryan@gmail.com> (https://github.com/Ryan-Carloso)",
55
+ "license": "MIT",
56
+ "bugs": {
57
+ "url": "https://github.com/Ryan-Carloso/react-native-onboarding-w-analytics/issues"
58
+ },
59
+ "homepage": "https://github.com/Ryan-Carloso/react-native-onboarding-w-analytics#readme",
60
+ "publishConfig": {
61
+ "registry": "https://registry.npmjs.org/"
62
+ },
63
+ "devDependencies": {
64
+ "@commitlint/config-conventional": "^19.8.1",
65
+ "@eslint/compat": "^1.3.2",
66
+ "@eslint/eslintrc": "^3.3.1",
67
+ "@eslint/js": "^9.35.0",
68
+ "@evilmartians/lefthook": "^1.12.3",
69
+ "@react-native/babel-preset": "0.81.1",
70
+ "@react-native/eslint-config": "^0.81.1",
71
+ "@release-it/conventional-changelog": "^10.0.1",
72
+ "@types/jest": "^29.5.14",
73
+ "@types/react": "^19.1.12",
74
+ "commitlint": "^19.8.1",
75
+ "del-cli": "^6.0.0",
76
+ "eslint": "^9.35.0",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "eslint-plugin-prettier": "^5.5.4",
79
+ "expo-image": "^3.0.8",
80
+ "jest": "^29.7.0",
81
+ "prettier": "^3.6.2",
82
+ "react": "19.1.0",
83
+ "react-native": "0.81.4",
84
+ "react-native-builder-bob": "^0.40.13",
85
+ "react-native-reanimated": "4",
86
+ "react-native-safe-area-context": "^5.6.1",
87
+ "react-native-svg": "^15.13.0",
88
+ "react-native-worklets": "^0.5.1",
89
+ "release-it": "^19.0.4",
90
+ "typescript": "5.6.3"
91
+ },
92
+ "peerDependencies": {
93
+ "expo-image": "*",
94
+ "react": "*",
95
+ "react-native": "*",
96
+ "react-native-reanimated": "*",
97
+ "react-native-safe-area-context": "*",
98
+ "react-native-svg": "*"
99
+ },
100
+ "peerDependenciesMeta": {
101
+ "expo-image": {
102
+ "optional": true
103
+ },
104
+ "react-native-svg": {
105
+ "optional": true
106
+ }
107
+ },
108
+ "workspaces": [
109
+ "example"
110
+ ],
111
+ "packageManager": "yarn@3.6.1",
112
+ "jest": {
113
+ "preset": "react-native",
114
+ "modulePathIgnorePatterns": [
115
+ "<rootDir>/example/node_modules",
116
+ "<rootDir>/lib/"
117
+ ]
118
+ },
119
+ "commitlint": {
120
+ "extends": [
121
+ "@commitlint/config-conventional"
122
+ ]
123
+ },
124
+ "release-it": {
125
+ "git": {
126
+ "commitMessage": "chore: release ${version}",
127
+ "tagName": "v${version}"
128
+ },
129
+ "npm": {
130
+ "publish": true
131
+ },
132
+ "github": {
133
+ "release": true
134
+ },
135
+ "plugins": {
136
+ "@release-it/conventional-changelog": {
137
+ "preset": {
138
+ "name": "angular"
139
+ }
140
+ }
141
+ }
142
+ },
143
+ "prettier": {
144
+ "quoteProps": "consistent",
145
+ "singleQuote": true,
146
+ "tabWidth": 2,
147
+ "trailingComma": "es5",
148
+ "useTabs": false
149
+ },
150
+ "react-native-builder-bob": {
151
+ "source": "src",
152
+ "output": "lib",
153
+ "targets": [
154
+ [
155
+ "module",
156
+ {
157
+ "esm": true
158
+ }
159
+ ],
160
+ [
161
+ "typescript",
162
+ {
163
+ "project": "tsconfig.build.json"
164
+ }
165
+ ]
166
+ ]
167
+ },
168
+ "create-react-native-library": {
169
+ "languages": "js",
170
+ "type": "library",
171
+ "version": "0.54.3"
172
+ },
173
+ "dependencies": {
174
+ "expo-constants": "^18.0.12",
175
+ "expo-localization": "^17.0.8"
176
+ }
177
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,35 @@
1
+ import ThemeProvider from './utils/ThemeContext';
2
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
3
+ import {
4
+ type OnboardingProps,
5
+ type OnboardingColors,
6
+ type OnboardingFonts,
7
+ type OnboardingIntroPanelProps,
8
+ type OnboardingStepPanelProps,
9
+ type OnboardingStep,
10
+ } from './spill-onboarding/types';
11
+ import SpillOnboarding from './spill-onboarding';
12
+ import { Platform } from 'react-native';
13
+ import React from 'react';
14
+
15
+ function Onboarding({ colors, fonts, ...props }: OnboardingProps) {
16
+ const SafeArea = Platform.OS === 'web' ? React.Fragment : SafeAreaProvider;
17
+
18
+ return (
19
+ <SafeArea>
20
+ <ThemeProvider colors={colors} fonts={fonts}>
21
+ <SpillOnboarding {...props} />
22
+ </ThemeProvider>
23
+ </SafeArea>
24
+ );
25
+ }
26
+
27
+ export default Onboarding;
28
+ export type {
29
+ OnboardingProps,
30
+ OnboardingColors,
31
+ OnboardingFonts,
32
+ OnboardingIntroPanelProps,
33
+ OnboardingStepPanelProps,
34
+ OnboardingStep,
35
+ };
@@ -0,0 +1,12 @@
1
+ import { type Image as ExpoImageType } from 'expo-image';
2
+
3
+ let ExpoImage: typeof ExpoImageType | null = null;
4
+
5
+ try {
6
+ const { Image } = require('expo-image');
7
+ ExpoImage = Image;
8
+ } catch {
9
+ // expo-image not available
10
+ }
11
+
12
+ export { ExpoImage };
@@ -0,0 +1,17 @@
1
+ import {
2
+ type Svg as RNSVGType,
3
+ type Path as RNSVGPathType,
4
+ } from 'react-native-svg';
5
+
6
+ let ReactNativeSVG: typeof RNSVGType | null = null;
7
+ let ReactNativeSVGPath: typeof RNSVGPathType | null = null;
8
+
9
+ try {
10
+ const { Svg, Path } = require('react-native-svg');
11
+ ReactNativeSVG = Svg;
12
+ ReactNativeSVGPath = Path;
13
+ } catch {
14
+ // react-native-svg not available
15
+ }
16
+
17
+ export { ReactNativeSVG, ReactNativeSVGPath };
@@ -0,0 +1,75 @@
1
+ import { Platform } from 'react-native';
2
+ import Constants from 'expo-constants';
3
+ import * as Localization from 'expo-localization';
4
+
5
+ // TODO: need to get an better domain
6
+ const API_URL = 'https://api.freesupabase.shop/api/track';
7
+
8
+ export const trackEvent = async (
9
+ apiKey: string | undefined,
10
+ eventType: string,
11
+ metaData: any = {}
12
+ ) => {
13
+ const token = apiKey;
14
+ const appName =
15
+ Constants.expoConfig?.name ||
16
+ Constants.manifest?.name ||
17
+ 'react-native-app';
18
+ const locales = Localization.getLocales();
19
+ const primaryLocale = locales[0];
20
+
21
+ const extendedMetaData = {
22
+ ...metaData,
23
+ locale: primaryLocale?.languageTag,
24
+ region: primaryLocale?.regionCode,
25
+ language: primaryLocale?.languageCode,
26
+ };
27
+
28
+ if (!token) {
29
+ console.error(
30
+ JSON.stringify({
31
+ error: 'Go to `https://freesupabase.shop/docs` to get an app token',
32
+ })
33
+ );
34
+ return;
35
+ }
36
+
37
+ const payload = {
38
+ app_id: token,
39
+ eventType,
40
+ userAgent: `React Native (${Platform.OS})`,
41
+ sourceUrl: appName,
42
+ metaData: extendedMetaData,
43
+ };
44
+
45
+ console.log('📡 Sending Analytics Event:', JSON.stringify(payload, null, 2));
46
+
47
+ try {
48
+ const response = await fetch(API_URL, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ 'Authorization': `Bearer ${token}`,
53
+ },
54
+ body: JSON.stringify(payload),
55
+ });
56
+
57
+ if (!response.ok) {
58
+ if (response.status === 401) {
59
+ console.error(
60
+ JSON.stringify({
61
+ error: 'Go to `https://freesupabase.shop/docs` to get an app token',
62
+ })
63
+ );
64
+ } else {
65
+ console.error(
66
+ 'Analytics error:',
67
+ response.status,
68
+ await response.text()
69
+ );
70
+ }
71
+ }
72
+ } catch (error) {
73
+ console.error('Failed to send analytics event:', error);
74
+ }
75
+ };
@@ -0,0 +1,70 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ TouchableOpacity,
4
+ Text,
5
+ StyleSheet,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ type GestureResponderEvent,
9
+ } from 'react-native';
10
+ import { useTheme } from '../../utils/ThemeContext';
11
+ import { fontSizes } from '../../utils/fontStyles';
12
+ import type { Theme } from '../../utils/theme';
13
+
14
+ interface Props {
15
+ text: string;
16
+ onPress: (event: GestureResponderEvent) => void;
17
+ icon?: React.ReactNode;
18
+ disabled?: boolean;
19
+ style?: ViewStyle;
20
+ textStyle?: TextStyle;
21
+ }
22
+
23
+ const PrimaryButton = ({
24
+ text,
25
+ onPress,
26
+ icon,
27
+ disabled = false,
28
+ style,
29
+ textStyle,
30
+ }: Props) => {
31
+ const { theme } = useTheme();
32
+ const styles = useMemo(
33
+ () => createStyles(theme, disabled),
34
+ [theme, disabled]
35
+ );
36
+
37
+ return (
38
+ <TouchableOpacity
39
+ onPress={onPress}
40
+ disabled={disabled}
41
+ style={[styles.button, style]}
42
+ >
43
+ {icon}
44
+ <Text style={[styles.text, textStyle]}>{text}</Text>
45
+ </TouchableOpacity>
46
+ );
47
+ };
48
+
49
+ export default PrimaryButton;
50
+
51
+ const createStyles = (theme: Theme, disabled: boolean) =>
52
+ StyleSheet.create({
53
+ button: {
54
+ height: 48,
55
+ width: '100%',
56
+ borderRadius: 12,
57
+ paddingHorizontal: 10,
58
+ justifyContent: 'center',
59
+ alignItems: 'center',
60
+ backgroundColor: theme.bg.primary,
61
+ opacity: disabled ? 0.4 : 1,
62
+ flexDirection: 'row',
63
+ gap: 2,
64
+ },
65
+ text: {
66
+ fontFamily: theme.fonts.primaryButton,
67
+ fontSize: fontSizes.md,
68
+ color: theme.text.contrast,
69
+ },
70
+ });
@@ -0,0 +1,71 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ TouchableOpacity,
4
+ Text,
5
+ StyleSheet,
6
+ type ViewStyle,
7
+ type TextStyle,
8
+ type GestureResponderEvent,
9
+ } from 'react-native';
10
+ import { useTheme } from '../../utils/ThemeContext';
11
+ import { fontSizes } from '../../utils/fontStyles';
12
+ import type { Theme } from '../../utils/theme';
13
+
14
+ interface Props {
15
+ text: string;
16
+ onPress: (event: GestureResponderEvent) => void;
17
+ icon?: React.ReactNode;
18
+ disabled?: boolean;
19
+ style?: ViewStyle;
20
+ textStyle?: TextStyle;
21
+ }
22
+
23
+ const SecondaryButton = ({
24
+ text,
25
+ onPress,
26
+ icon,
27
+ disabled = false,
28
+ style,
29
+ textStyle,
30
+ }: Props) => {
31
+ const { theme } = useTheme();
32
+ const styles = useMemo(
33
+ () => createStyles(theme, disabled),
34
+ [theme, disabled]
35
+ );
36
+
37
+ return (
38
+ <TouchableOpacity
39
+ onPress={onPress}
40
+ disabled={disabled}
41
+ style={[styles.button, style]}
42
+ >
43
+ {icon}
44
+ {!!text && <Text style={[styles.text, textStyle]}>{text}</Text>}
45
+ </TouchableOpacity>
46
+ );
47
+ };
48
+
49
+ export default SecondaryButton;
50
+
51
+ const createStyles = (theme: Theme, disabled: boolean) =>
52
+ StyleSheet.create({
53
+ button: {
54
+ height: 48,
55
+ paddingHorizontal: 10,
56
+ alignItems: 'center',
57
+ justifyContent: 'center',
58
+ borderWidth: 1,
59
+ borderRadius: 12,
60
+ borderColor: theme.bg.accent,
61
+ opacity: disabled ? 0.4 : 1,
62
+ flexDirection: 'row',
63
+ gap: 2,
64
+ },
65
+ text: {
66
+ fontFamily: theme.fonts.secondaryButton,
67
+ fontSize: fontSizes.sm,
68
+ color: theme.text.primary,
69
+ lineHeight: 20,
70
+ },
71
+ });
@@ -0,0 +1,34 @@
1
+ import { useMemo } from 'react';
2
+ import { StyleSheet, TouchableOpacity } from 'react-native';
3
+ import { useTheme } from '../../utils/ThemeContext';
4
+ import { type Theme } from '../../utils/theme';
5
+ import CloseIcon from '../icons/CloseIcon';
6
+
7
+ interface Props {
8
+ onPress: () => void;
9
+ }
10
+
11
+ function SkipButton({ onPress }: Props) {
12
+ const { theme } = useTheme();
13
+ const styles = useMemo(() => createStyles(theme), [theme]);
14
+
15
+ return (
16
+ <TouchableOpacity style={styles.wrapper} onPress={onPress}>
17
+ <CloseIcon color={theme.text.primary} size={24} />
18
+ </TouchableOpacity>
19
+ );
20
+ }
21
+
22
+ export default SkipButton;
23
+
24
+ const createStyles = (theme: Theme) =>
25
+ StyleSheet.create({
26
+ wrapper: {
27
+ backgroundColor: theme.bg.secondary,
28
+ width: 32,
29
+ height: 32,
30
+ borderRadius: 6,
31
+ justifyContent: 'center',
32
+ alignItems: 'center',
33
+ },
34
+ });
@@ -0,0 +1,181 @@
1
+ import { useMemo, type ReactNode } from 'react';
2
+ import {
3
+ Image,
4
+ type ImageSourcePropType,
5
+ Platform,
6
+ StyleSheet,
7
+ useWindowDimensions,
8
+ } from 'react-native';
9
+ import Animated, {
10
+ interpolate,
11
+ useDerivedValue,
12
+ useAnimatedStyle,
13
+ withTiming,
14
+ Easing,
15
+ type SharedValue,
16
+ useSharedValue,
17
+ } from 'react-native-reanimated';
18
+ import { useTheme } from '../../utils/ThemeContext';
19
+ import useMeasureHeight from '../hooks/useMeasureHeight';
20
+ import type { Theme } from '../../utils/theme';
21
+ import type { OnboardingStep } from '../types';
22
+ import { ExpoImage } from '../adapters/expo-image';
23
+
24
+ interface OnboardingImageContainerProps {
25
+ currentStep: OnboardingStep | undefined;
26
+ currentStepImage: ImageSourcePropType | undefined;
27
+ position: 'top' | 'bottom';
28
+ animationDuration: number;
29
+ backgroundSpillProgress: SharedValue<number>;
30
+ introPanel: any;
31
+ stepPanel: any;
32
+ screenHeight: number;
33
+ background?: () => ReactNode;
34
+ }
35
+
36
+ function OnboardingImageContainer({
37
+ currentStep,
38
+ currentStepImage,
39
+ position,
40
+ animationDuration,
41
+ backgroundSpillProgress,
42
+ introPanel,
43
+ stepPanel,
44
+ screenHeight,
45
+ background,
46
+ }: OnboardingImageContainerProps) {
47
+ const { theme } = useTheme();
48
+ const image = useMeasureHeight();
49
+ const styles = useMemo(() => createStyles(theme), [theme]);
50
+ const extraPadding = Platform.OS === 'web' ? 16 : 0;
51
+ const { width: screenWidth } = useWindowDimensions();
52
+
53
+ // bottom panel height (intro or step)
54
+ const bottomPanelHeight = (currentStep ? stepPanel : introPanel).height || 0;
55
+
56
+ /**
57
+ * When 0 -> no spill; when 1 -> fully spilled by bottomPanelHeight
58
+ */
59
+ const backgroundSpillDistance = useDerivedValue(() =>
60
+ interpolate(backgroundSpillProgress.value, [0, 1], [0, bottomPanelHeight])
61
+ );
62
+
63
+ const imageWrapperHeight = useDerivedValue(
64
+ () => screenHeight - bottomPanelHeight + backgroundSpillDistance.value
65
+ );
66
+
67
+ const imageWrapperAnimation = useAnimatedStyle(() => ({
68
+ height: imageWrapperHeight.value,
69
+ }));
70
+
71
+ const imageTargetY = useDerivedValue(() => {
72
+ const topSafe = theme.insets.top + extraPadding + 16;
73
+
74
+ if (position === 'top') {
75
+ return topSafe;
76
+ }
77
+
78
+ const imageH = image.height || 0;
79
+ const modalTop = screenHeight - bottomPanelHeight - 16;
80
+ const yBottom = modalTop - imageH;
81
+
82
+ const shouldClampTop = !currentStep;
83
+ return shouldClampTop ? Math.max(yBottom, topSafe) : yBottom;
84
+ });
85
+
86
+ const hasMounted = useSharedValue(false);
87
+
88
+ const imageAnimation = useAnimatedStyle(() => {
89
+ const translateY = withTiming(
90
+ imageTargetY.value,
91
+ {
92
+ duration: hasMounted.value ? animationDuration : 0,
93
+ easing: Easing.out(Easing.cubic),
94
+ },
95
+ () => (hasMounted.value = true)
96
+ );
97
+ const sideEdges = Math.max(32 + 24 - backgroundSpillDistance.value, 0);
98
+
99
+ return {
100
+ transform: [{ translateY }],
101
+ maxWidth: screenWidth - sideEdges,
102
+ };
103
+ }, [animationDuration]);
104
+
105
+ const backgroundAnimation = useAnimatedStyle(() => {
106
+ const topEdge = Math.max(
107
+ theme.insets.top + extraPadding - backgroundSpillDistance.value,
108
+ 0
109
+ );
110
+ const sideEdge = Math.max(16 - backgroundSpillDistance.value, 0);
111
+
112
+ return {
113
+ position: 'absolute',
114
+ top: topEdge,
115
+ left: sideEdge,
116
+ right: sideEdge,
117
+ bottom: screenHeight - imageWrapperHeight.value,
118
+ borderRadius: Math.max(12 - backgroundSpillDistance.value, 0),
119
+ };
120
+ });
121
+
122
+ return (
123
+ <>
124
+ {background ? (
125
+ <Animated.View style={backgroundAnimation}>
126
+ {background()}
127
+ </Animated.View>
128
+ ) : (
129
+ <Animated.View style={[styles.colorBg, backgroundAnimation]} />
130
+ )}
131
+
132
+ {currentStepImage && (
133
+ <Animated.View style={[styles.imageWrapper, imageWrapperAnimation]}>
134
+ <Animated.View style={[styles.image, imageAnimation]} ref={image.ref}>
135
+ {ExpoImage ? (
136
+ <ExpoImage
137
+ source={currentStepImage}
138
+ contentFit="contain"
139
+ transition={0}
140
+ style={styles.imageStyle}
141
+ />
142
+ ) : (
143
+ <Image
144
+ source={currentStepImage}
145
+ resizeMode="contain"
146
+ fadeDuration={0}
147
+ style={styles.imageStyle}
148
+ />
149
+ )}
150
+ </Animated.View>
151
+ </Animated.View>
152
+ )}
153
+ </>
154
+ );
155
+ }
156
+
157
+ export default OnboardingImageContainer;
158
+
159
+ const createStyles = (theme: Theme) =>
160
+ StyleSheet.create({
161
+ colorBg: {
162
+ backgroundColor: theme.bg.primary,
163
+ overflow: 'hidden',
164
+ },
165
+ imageWrapper: {
166
+ position: 'absolute',
167
+ top: 0,
168
+ left: 0,
169
+ right: 0,
170
+ overflow: 'hidden',
171
+ },
172
+ image: {
173
+ alignSelf: 'center',
174
+ alignItems: 'center',
175
+ overflow: 'hidden',
176
+ },
177
+ imageStyle: {
178
+ width: 300,
179
+ height: 600,
180
+ },
181
+ });