├── .watchmanconfig ├── .nvmrc ├── .gitattributes ├── tsconfig.build.json ├── assets ├── modal.png ├── step.png ├── banner.png ├── back-button.png ├── intro-panel.png ├── private-mind.png └── skip-button.png ├── example ├── assets │ ├── icon.png │ ├── logo.png │ ├── favicon.png │ ├── splash-icon.png │ ├── adaptive-icon.png │ ├── checklist │ │ ├── create.png │ │ ├── share.png │ │ └── categorize.png │ └── onboarding │ │ ├── step_chat.png │ │ ├── step_models.png │ │ ├── step_voice.png │ │ ├── step_chat@2x.png │ │ ├── step_chat@3x.png │ │ ├── step_sources.png │ │ ├── step_voice@2x.png │ │ ├── step_voice@3x.png │ │ ├── step_benchmark.png │ │ ├── step_models@2x.png │ │ ├── step_models@3x.png │ │ ├── step_sources@2x.png │ │ ├── step_sources@3x.png │ │ ├── step_benchmark@2x.png │ │ └── step_benchmark@3x.png ├── tsconfig.json ├── index.js ├── babel.config.js ├── app.json ├── metro.config.js ├── package.json └── src │ ├── App.tsx │ └── screens │ ├── OnboardingCustomTheme.tsx │ ├── OnboardingDefault.tsx │ ├── OnboardingChecklist.tsx │ ├── OnboardingCustomIntro.tsx │ ├── Home.tsx │ ├── OnboardingCustomSteps.tsx │ └── OnboardingGradient.tsx ├── .cursor └── mcp.json ├── src ├── utils │ ├── fontStyles.ts │ ├── theme.ts │ └── ThemeContext.tsx ├── spill-onboarding │ ├── adapters │ │ ├── expo-image.ts │ │ └── react-native-svg.ts │ ├── hooks │ │ └── useMeasureHeight.ts │ ├── buttons │ │ ├── SkipButton.tsx │ │ ├── PrimaryButton.tsx │ │ └── SecondaryButton.tsx │ ├── icons │ │ ├── CloseIcon.tsx │ │ └── ArrowLeftIcon.tsx │ ├── components │ │ ├── OnboardingModal.tsx │ │ ├── OnboardingStepContainer.tsx │ │ ├── OnboardingIntroPanel.tsx │ │ ├── OnboardingStepPanel.tsx │ │ └── OnboardingImageContainer.tsx │ ├── index.tsx │ └── types.ts └── index.tsx ├── babel.config.js ├── lefthook.yml ├── .editorconfig ├── .yarnrc.yml ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml ├── actions │ └── setup │ │ └── action.yml └── workflows │ └── ci.yml ├── eslint.config.mjs ├── tsconfig.json ├── LICENSE ├── .gitignore ├── package.json ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── README.md └── .yarn └── plugins └── @yarnpkg └── plugin-workspace-tools.cjs /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.19.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf 4 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example", "lib"] 4 | } 5 | -------------------------------------------------------------------------------- /assets/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/modal.png -------------------------------------------------------------------------------- /assets/step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/step.png -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/banner.png -------------------------------------------------------------------------------- /assets/back-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/back-button.png -------------------------------------------------------------------------------- /assets/intro-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/intro-panel.png -------------------------------------------------------------------------------- /assets/private-mind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/private-mind.png -------------------------------------------------------------------------------- /assets/skip-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/assets/skip-button.png -------------------------------------------------------------------------------- /example/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/icon.png -------------------------------------------------------------------------------- /example/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/logo.png -------------------------------------------------------------------------------- /example/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/favicon.png -------------------------------------------------------------------------------- /example/assets/splash-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/splash-icon.png -------------------------------------------------------------------------------- /example/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/adaptive-icon.png -------------------------------------------------------------------------------- /example/assets/checklist/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/checklist/create.png -------------------------------------------------------------------------------- /example/assets/checklist/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/checklist/share.png -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | // Avoid expo-cli auto-generating a tsconfig 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/assets/checklist/categorize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/checklist/categorize.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_chat.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_models.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_models.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_voice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_voice.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_chat@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_chat@2x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_chat@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_chat@3x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_sources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_sources.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_voice@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_voice@2x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_voice@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_voice@3x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_benchmark.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_models@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_models@2x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_models@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_models@3x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_sources@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_sources@2x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_sources@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_sources@3x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_benchmark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_benchmark@2x.png -------------------------------------------------------------------------------- /example/assets/onboarding/step_benchmark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/software-mansion-labs/react-native-onboarding/HEAD/example/assets/onboarding/step_benchmark@3x.png -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "RadonAi": { 4 | "url": "http://127.0.0.1:60544/mcp", 5 | "type": "http", 6 | "headers": { 7 | "nonce": "20f25da4-4227-4b6e-8cf1-9f26afcf7289" 8 | } 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/utils/fontStyles.ts: -------------------------------------------------------------------------------- 1 | export const fontSizes = { 2 | xxl: 34, 3 | xl: 22, 4 | lg: 18, 5 | md: 16, 6 | sm: 14, 7 | xs: 12, 8 | xxs: 10, 9 | }; 10 | 11 | export const lineHeights = { 12 | xxl: 40, 13 | xl: 28, 14 | lg: 24, 15 | md: 24, 16 | sm: 20, 17 | xs: 16, 18 | xxs: 12, 19 | }; 20 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | exclude: /\/node_modules\//, 5 | presets: ['module:react-native-builder-bob/babel-preset'], 6 | }, 7 | { 8 | include: /\/node_modules\//, 9 | presets: ['module:@react-native/babel-preset'], 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /src/spill-onboarding/adapters/expo-image.ts: -------------------------------------------------------------------------------- 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 }; 13 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | glob: "*.{js,ts,jsx,tsx}" 6 | run: npx eslint {staged_files} 7 | types: 8 | glob: "*.{js,ts, jsx, tsx}" 9 | run: npx tsc 10 | commit-msg: 11 | parallel: true 12 | commands: 13 | commitlint: 14 | run: npx commitlint --edit 15 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './src/App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | plugins: 5 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 6 | spec: "@yarnpkg/plugin-interactive-tools" 7 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 8 | spec: "@yarnpkg/plugin-workspace-tools" 9 | 10 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 11 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getConfig } = require('react-native-builder-bob/babel-config'); 3 | const pkg = require('../package.json'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | module.exports = function (api) { 8 | api.cache(true); 9 | 10 | return getConfig( 11 | { 12 | presets: ['babel-preset-expo'], 13 | }, 14 | { root, pkg } 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Feature Request 💡 4 | url: https://github.com/software-mansion-labs/react-native-onboarding/discussions/new?category=ideas 5 | about: If you have a feature request, please create a new discussion on GitHub. 6 | - name: Discussions on GitHub 💬 7 | url: https://github.com/software-mansion-labs/react-native-onboarding/discussions 8 | about: If this library works as promised but you need help, please ask questions there. 9 | -------------------------------------------------------------------------------- /src/spill-onboarding/adapters/react-native-svg.ts: -------------------------------------------------------------------------------- 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 }; 18 | -------------------------------------------------------------------------------- /src/spill-onboarding/hooks/useMeasureHeight.ts: -------------------------------------------------------------------------------- 1 | import { useLayoutEffect, useRef, useState } from 'react'; 2 | import type { View } from 'react-native'; 3 | import type Reanimated from 'react-native-reanimated'; 4 | 5 | export type ViewRef = React.ComponentRef & 6 | React.ComponentRef; 7 | 8 | function useMeasureHeight() { 9 | const ref = useRef(null); 10 | const [height, setHeight] = useState(0); 11 | 12 | useLayoutEffect(() => { 13 | ref.current?.measure?.((_, __, ___, viewHeight) => { 14 | setHeight(viewHeight); 15 | }); 16 | }); 17 | 18 | return { ref, height }; 19 | } 20 | 21 | export default useMeasureHeight; 22 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 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 }; 30 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "example", 4 | "slug": "example", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "userInterfaceStyle": "light", 9 | "newArchEnabled": true, 10 | "splash": { 11 | "image": "./assets/splash-icon.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true, 17 | "bundleIdentifier": "onboarding.example" 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | }, 24 | "edgeToEdgeEnabled": true, 25 | "package": "onboarding.example" 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getDefaultConfig } = require('@expo/metro-config'); 3 | const { withMetroConfig } = require('react-native-monorepo-config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | /** 8 | * Metro configuration 9 | * https://facebook.github.io/metro/docs/configuration 10 | * 11 | * @type {import('metro-config').MetroConfig} 12 | */ 13 | const config = withMetroConfig(getDefaultConfig(__dirname), { 14 | root, 15 | dirname: __dirname, 16 | }); 17 | 18 | config.resolver.unstable_enablePackageExports = true; 19 | 20 | // Add watchFolders to include the parent directory for asset resolution 21 | config.watchFolders = [root]; 22 | 23 | // Configure asset extensions 24 | config.resolver.assetExts = [ 25 | ...config.resolver.assetExts, 26 | 'png', 27 | 'jpg', 28 | 'jpeg', 29 | 'gif', 30 | 'webp', 31 | 'svg', 32 | ]; 33 | 34 | module.exports = config; 35 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import { FlatCompat } from '@eslint/eslintrc'; 3 | import js from '@eslint/js'; 4 | import prettier from 'eslint-plugin-prettier'; 5 | import { defineConfig } from 'eslint/config'; 6 | import path from 'node:path'; 7 | import { fileURLToPath } from 'node:url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default defineConfig([ 18 | { 19 | extends: fixupConfigRules(compat.extends('@react-native', 'prettier')), 20 | plugins: { prettier }, 21 | rules: { 22 | 'react/react-in-jsx-scope': 'off', 23 | 'prettier/prettier': 'error', 24 | }, 25 | }, 26 | { 27 | ignores: ['node_modules/', 'lib/'], 28 | }, 29 | ]); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "paths": { 5 | "@blazejkustra/react-native-onboarding": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "customConditions": ["react-native-strict-api"], 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react-jsx", 13 | "lib": ["ESNext"], 14 | "module": "ESNext", 15 | "moduleResolution": "bundler", 16 | "noEmit": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitReturns": true, 19 | "noImplicitUseStrict": false, 20 | "noStrictGenericChecks": false, 21 | "noUncheckedIndexedAccess": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "strict": true, 27 | "target": "ESNext", 28 | "verbatimModuleSyntax": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/spill-onboarding/buttons/SkipButton.tsx: -------------------------------------------------------------------------------- 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 | 17 | 18 | 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 | }); 35 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 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 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | export default Onboarding; 28 | export type { 29 | OnboardingProps, 30 | OnboardingColors, 31 | OnboardingFonts, 32 | OnboardingIntroPanelProps, 33 | OnboardingStepPanelProps, 34 | OnboardingStep, 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Blazej Kustra 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@blazejkustra/react-native-onboarding-example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web" 10 | }, 11 | "dependencies": { 12 | "@expo/metro-runtime": "~6.1.2", 13 | "@react-native-async-storage/async-storage": "^2.2.0", 14 | "@react-navigation/native": "^7.1.17", 15 | "@react-navigation/native-stack": "^7.3.26", 16 | "expo": "~54.0.7", 17 | "expo-image": "^3.0.8", 18 | "expo-linear-gradient": "~14.0.1", 19 | "expo-status-bar": "~3.0.8", 20 | "react": "19.1.0", 21 | "react-dom": "19.1.0", 22 | "react-native": "0.81.4", 23 | "react-native-reanimated": "^4.1.2", 24 | "react-native-safe-area-context": "^5.6.1", 25 | "react-native-screens": "^4.16.0", 26 | "react-native-web": "~0.21.0", 27 | "react-native-worklets": "0.5.0" 28 | }, 29 | "private": true, 30 | "devDependencies": { 31 | "react-native-builder-bob": "^0.40.13", 32 | "react-native-monorepo-config": "^0.1.9" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | **/.xcode.env.local 32 | 33 | # Android/IJ 34 | # 35 | .classpath 36 | .cxx 37 | .gradle 38 | .idea 39 | .project 40 | .settings 41 | local.properties 42 | android.iml 43 | 44 | # Cocoapods 45 | # 46 | example/ios/Pods 47 | 48 | # Ruby 49 | example/vendor/ 50 | 51 | # node.js 52 | # 53 | node_modules/ 54 | npm-debug.log 55 | yarn-debug.log 56 | yarn-error.log 57 | 58 | # BUCK 59 | buck-out/ 60 | \.buckd/ 61 | android/app/libs 62 | android/keystores/debug.keystore 63 | 64 | # Yarn 65 | .yarn/* 66 | !.yarn/patches 67 | !.yarn/plugins 68 | !.yarn/releases 69 | !.yarn/sdks 70 | !.yarn/versions 71 | 72 | # Expo 73 | .expo/ 74 | 75 | # Turborepo 76 | .turbo/ 77 | 78 | # generated by bob 79 | lib/ 80 | 81 | # React Native Codegen 82 | ios/generated 83 | android/generated 84 | 85 | # React Native Nitro Modules 86 | nitrogen/ 87 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup Node.js and install dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Setup Node.js 8 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 9 | with: 10 | node-version-file: .nvmrc 11 | 12 | - name: Restore dependencies 13 | id: yarn-cache 14 | uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 15 | with: 16 | path: | 17 | **/node_modules 18 | .yarn/install-state.gz 19 | key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}-${{ hashFiles('**/package.json', '!node_modules/**') }} 20 | restore-keys: | 21 | ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} 22 | ${{ runner.os }}-yarn- 23 | 24 | - name: Install dependencies 25 | if: steps.yarn-cache.outputs.cache-hit != 'true' 26 | run: yarn install --immutable 27 | shell: bash 28 | 29 | - name: Cache dependencies 30 | if: steps.yarn-cache.outputs.cache-hit != 'true' 31 | uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 32 | with: 33 | path: | 34 | **/node_modules 35 | .yarn/install-state.gz 36 | key: ${{ steps.yarn-cache.outputs.cache-primary-key }} 37 | -------------------------------------------------------------------------------- /src/spill-onboarding/icons/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import { View, StyleSheet } from 'react-native'; 2 | 3 | interface CloseIconProps { 4 | size?: number; 5 | color?: string; 6 | } 7 | 8 | export default function CloseIcon({ 9 | size = 24, 10 | color = '#000', 11 | }: CloseIconProps) { 12 | const lineWidth = Math.max(2, size * 0.1); 13 | const lineLength = size * 0.7; 14 | 15 | return ( 16 | 17 | {/* First diagonal line */} 18 | 29 | {/* Second diagonal line */} 30 | 43 | 44 | ); 45 | } 46 | 47 | const styles = StyleSheet.create({ 48 | container: { 49 | justifyContent: 'center', 50 | alignItems: 'center', 51 | }, 52 | line: { 53 | position: 'absolute', 54 | }, 55 | }); 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | merge_group: 10 | types: 11 | - checks_requested 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | 25 | - name: Setup 26 | uses: ./.github/actions/setup 27 | 28 | - name: Lint files 29 | run: yarn lint 30 | 31 | - name: Typecheck files 32 | run: yarn typecheck 33 | 34 | test: 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 40 | 41 | - name: Setup 42 | uses: ./.github/actions/setup 43 | 44 | - name: Run unit tests 45 | run: yarn test --maxWorkers=2 --coverage 46 | 47 | build-library: 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 53 | 54 | - name: Setup 55 | uses: ./.github/actions/setup 56 | 57 | - name: Build package 58 | run: yarn prepare 59 | 60 | build-web: 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - name: Checkout 65 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 66 | 67 | - name: Setup 68 | uses: ./.github/actions/setup 69 | 70 | - name: Build example for Web 71 | run: | 72 | yarn example expo export --platform web 73 | -------------------------------------------------------------------------------- /src/spill-onboarding/buttons/PrimaryButton.tsx: -------------------------------------------------------------------------------- 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 | 43 | {icon} 44 | {text} 45 | 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 | }); 71 | -------------------------------------------------------------------------------- /src/spill-onboarding/buttons/SecondaryButton.tsx: -------------------------------------------------------------------------------- 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 | 43 | {icon} 44 | {!!text && {text}} 45 | 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 | }); 72 | -------------------------------------------------------------------------------- /src/spill-onboarding/components/OnboardingModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { 3 | View, 4 | StyleSheet, 5 | Modal, 6 | TouchableOpacity, 7 | useWindowDimensions, 8 | } from 'react-native'; 9 | import { useTheme } from '../../utils/ThemeContext'; 10 | import type { Theme } from '../../utils/theme'; 11 | 12 | interface OnboardingModalProps { 13 | onSkip?: () => void; 14 | children: React.ReactNode; 15 | } 16 | 17 | export default function OnboardingModal({ 18 | onSkip, 19 | children, 20 | }: OnboardingModalProps) { 21 | const { theme } = useTheme(); 22 | const { height, width } = useWindowDimensions(); 23 | const styles = useMemo( 24 | () => createStyles(theme, height, width), 25 | [height, width, theme] 26 | ); 27 | 28 | return ( 29 | 30 | 31 | 36 | {children} 37 | 38 | 39 | ); 40 | } 41 | 42 | const createStyles = (theme: Theme, height: number, width: number) => 43 | StyleSheet.create({ 44 | webOverlay: { 45 | flex: 1, 46 | backgroundColor: 'rgba(0, 0, 0, 0.5)', 47 | justifyContent: 'center', 48 | alignItems: 'center', 49 | }, 50 | webBackdrop: { 51 | position: 'absolute', 52 | top: 0, 53 | left: 0, 54 | right: 0, 55 | bottom: 0, 56 | }, 57 | webModal: { 58 | width: Math.min(width, 500), 59 | height: Math.min(height, 800), 60 | borderRadius: width > 500 ? 28 : 0, 61 | shadowColor: '#000', 62 | shadowOffset: { 63 | width: 0, 64 | height: 10, 65 | }, 66 | shadowOpacity: 0.25, 67 | shadowRadius: 20, 68 | overflow: 'hidden', 69 | backgroundColor: theme.bg.secondary, 70 | }, 71 | webContent: { 72 | flex: 1, 73 | paddingTop: 16, 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /src/spill-onboarding/icons/ArrowLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Text, StyleSheet } from 'react-native'; 2 | import { ExpoImage } from '../adapters/expo-image'; 3 | import { 4 | ReactNativeSVG, 5 | ReactNativeSVGPath, 6 | } from '../adapters/react-native-svg'; 7 | 8 | interface ArrowLeftIconProps { 9 | size?: number; 10 | color?: string; 11 | } 12 | 13 | const SVG_ARROW_LEFT_STRING = 14 | 'M10.7071 5.29289C11.0976 5.68342 11.0976 6.31658 10.7071 6.70711L6.41421 11L20 11C20.5523 11 21 11.4477 21 12C21 12.5523 20.5523 13 20 13L6.41421 13L10.7071 17.2929C11.0976 17.6834 11.0976 18.3166 10.7071 18.7071C10.3166 19.0976 9.68342 19.0976 9.29289 18.7071L3.29289 12.7071C3.10536 12.5196 3 12.2652 3 12C3 11.7348 3.10536 11.4804 3.29289 11.2929L9.29289 5.29289C9.68342 4.90237 10.3166 4.90237 10.7071 5.29289Z'; 15 | 16 | export default function ArrowLeftIcon({ 17 | size = 24, 18 | color = '#000', 19 | }: ArrowLeftIconProps) { 20 | // Use expo-image with dynamic SVG string (includes color) 21 | if (ExpoImage) { 22 | const svgString = ` 23 | 24 | `; 25 | 26 | return ( 27 | 32 | ); 33 | } 34 | 35 | // Use SVG if available 36 | if (ReactNativeSVG && ReactNativeSVGPath) { 37 | return ( 38 | 44 | 45 | 46 | ); 47 | } 48 | 49 | // Fallback to text arrow 50 | return ( 51 | 60 | Go Back 61 | 62 | ); 63 | } 64 | 65 | const styles = StyleSheet.create({ 66 | arrow: { 67 | textAlign: 'center', 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationContainer } from '@react-navigation/native'; 2 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 3 | import { enableScreens } from 'react-native-screens'; 4 | import Home from './screens/Home'; 5 | import OnboardingDefault from './screens/OnboardingDefault'; 6 | import OnboardingCustomIntro from './screens/OnboardingCustomIntro'; 7 | import OnboardingCustomSteps from './screens/OnboardingCustomSteps'; 8 | import OnboardingCustomTheme from './screens/OnboardingCustomTheme'; 9 | import OnboardingGradient from './screens/OnboardingGradient'; 10 | import OnboardingChecklist from './screens/OnboardingChecklist'; 11 | 12 | enableScreens(true); 13 | 14 | export type RootStackParamList = { 15 | Home: undefined; 16 | OnboardingDefault: undefined; 17 | OnboardingCustomIntro: undefined; 18 | OnboardingCustomSteps: undefined; 19 | OnboardingCustomTheme: undefined; 20 | OnboardingGradient: undefined; 21 | OnboardingChecklist: undefined; 22 | }; 23 | 24 | const Stack = createNativeStackNavigator(); 25 | 26 | export default function App() { 27 | return ( 28 | 29 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Report a reproducible bug or regression in this library. 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # Bug report 9 | 10 | 👋 Hi! 11 | 12 | **Please fill the following carefully before opening a new issue ❗** 13 | *(Your issue may be closed if it doesn't provide the required pieces of information)* 14 | - type: checkboxes 15 | attributes: 16 | label: Before submitting a new issue 17 | description: Please perform simple checks first. 18 | options: 19 | - label: I tested using the latest version of the library, as the bug might be already fixed. 20 | required: true 21 | - label: I tested using a [supported version](https://github.com/reactwg/react-native-releases/blob/main/docs/support.md) of react native. 22 | required: true 23 | - label: I checked for possible duplicate issues, with possible answers. 24 | required: true 25 | - type: textarea 26 | id: summary 27 | attributes: 28 | label: Bug summary 29 | description: | 30 | Provide a clear and concise description of what the bug is. 31 | If needed, you can also provide other samples: error messages / stack traces, screenshots, gifs, etc. 32 | validations: 33 | required: true 34 | - type: input 35 | id: library-version 36 | attributes: 37 | label: Library version 38 | description: What version of the library are you using? 39 | placeholder: "x.x.x" 40 | validations: 41 | required: true 42 | - type: textarea 43 | id: react-native-info 44 | attributes: 45 | label: Environment info 46 | description: Run `react-native info` in your terminal and paste the results here. 47 | render: shell 48 | validations: 49 | required: true 50 | - type: textarea 51 | id: steps-to-reproduce 52 | attributes: 53 | label: Steps to reproduce 54 | description: | 55 | You must provide a clear list of steps and code to reproduce the problem. 56 | value: | 57 | 1. … 58 | 2. … 59 | validations: 60 | required: true 61 | - type: input 62 | id: reproducible-example 63 | attributes: 64 | label: Reproducible example repository 65 | description: Please provide a link to a repository on GitHub with a reproducible example. 66 | validations: 67 | required: true 68 | -------------------------------------------------------------------------------- /src/spill-onboarding/components/OnboardingStepContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, type ReactNode } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | import Reanimated, { 4 | FadeIn, 5 | FadeInDown, 6 | FadeOut, 7 | FadeOutDown, 8 | } from 'react-native-reanimated'; 9 | import SkipButton from '../buttons/SkipButton'; 10 | import { useTheme } from '../../utils/ThemeContext'; 11 | import type { Theme } from '../../utils/theme'; 12 | import type { OnboardingStep } from '../types'; 13 | 14 | interface OnboardingStepContainerProps { 15 | currentStep: OnboardingStep | undefined; 16 | showCloseButton?: boolean; 17 | animationDuration: number; 18 | onSkip?: () => void; 19 | ref: React.RefObject; 20 | renderStepContent: () => React.ReactNode; 21 | skipButton?: ({ onPress }: { onPress: () => void }) => ReactNode; 22 | } 23 | 24 | function OnboardingStepContainer({ 25 | currentStep, 26 | showCloseButton, 27 | animationDuration, 28 | onSkip, 29 | ref, 30 | renderStepContent, 31 | skipButton, 32 | }: OnboardingStepContainerProps) { 33 | const { theme } = useTheme(); 34 | const styles = useMemo(() => createStyles(theme), [theme]); 35 | 36 | if (!currentStep) { 37 | return null; 38 | } 39 | 40 | return ( 41 | <> 42 | {showCloseButton && onSkip && ( 43 | 48 | {skipButton ? ( 49 | skipButton({ onPress: onSkip }) 50 | ) : ( 51 | 52 | )} 53 | 54 | )} 55 | 56 | 62 | {renderStepContent()} 63 | 64 | 65 | ); 66 | } 67 | 68 | export default OnboardingStepContainer; 69 | 70 | const createStyles = (theme: Theme) => 71 | StyleSheet.create({ 72 | bottomPanel: { 73 | paddingHorizontal: 16, 74 | paddingBottom: 16 + theme.insets.bottom, 75 | position: 'absolute', 76 | bottom: 0, 77 | left: 0, 78 | right: 0, 79 | }, 80 | close: { 81 | position: 'absolute', 82 | top: theme.insets.top + 16, 83 | right: 16, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /example/src/screens/OnboardingCustomTheme.tsx: -------------------------------------------------------------------------------- 1 | import Onboarding from '@blazejkustra/react-native-onboarding'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 4 | import type { RootStackParamList } from '../App'; 5 | 6 | type Props = NativeStackScreenProps< 7 | RootStackParamList, 8 | 'OnboardingCustomTheme' 9 | >; 10 | 11 | const STORAGE_KEY = 'onboarding_finished'; 12 | 13 | export default function OnboardingCustomTheme({ navigation }: Props) { 14 | return ( 15 | { 64 | await AsyncStorage.setItem(STORAGE_KEY, 'true'); 65 | navigation.goBack(); 66 | }} 67 | onSkip={() => navigation.goBack()} 68 | onStepChange={() => {}} 69 | showCloseButton 70 | showBackButton 71 | /> 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/spill-onboarding/components/OnboardingIntroPanel.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Image, StyleSheet, Text, View } from 'react-native'; 3 | import { useTheme } from '../../utils/ThemeContext'; 4 | import type { Theme } from '../../utils/theme'; 5 | import PrimaryButton from '../buttons/PrimaryButton'; 6 | import { fontSizes, lineHeights } from '../../utils/fontStyles'; 7 | import type { OnboardingIntroPanelProps } from '../types'; 8 | 9 | function OnboardingIntroPanel({ 10 | onPressStart, 11 | title, 12 | subtitle, 13 | button, 14 | image, 15 | }: OnboardingIntroPanelProps) { 16 | const { theme } = useTheme(); 17 | const styles = useMemo(() => createStyles(theme), [theme]); 18 | 19 | const renderTitle = () => { 20 | if (!title) { 21 | return undefined; 22 | } 23 | 24 | if (typeof title === 'string') { 25 | return ( 26 | 27 | {title} 28 | 29 | ); 30 | } 31 | 32 | return title; 33 | }; 34 | 35 | const renderSubtitle = () => { 36 | if (!subtitle) { 37 | return undefined; 38 | } 39 | 40 | if (typeof subtitle === 'string') { 41 | return ( 42 | 43 | {subtitle} 44 | 45 | ); 46 | } 47 | 48 | return subtitle; 49 | }; 50 | 51 | const renderButton = () => { 52 | if (typeof button === 'string') { 53 | return ; 54 | } 55 | 56 | return button({ onPressStart }); 57 | }; 58 | 59 | return ( 60 | 61 | {typeof image === 'function' 62 | ? image() 63 | : image && } 64 | 65 | {renderTitle()} 66 | {renderSubtitle()} 67 | 68 | {renderButton()} 69 | 70 | ); 71 | } 72 | 73 | export default OnboardingIntroPanel; 74 | 75 | const createStyles = (theme: Theme) => 76 | StyleSheet.create({ 77 | container: { 78 | marginTop: 16, 79 | }, 80 | image: { 81 | alignSelf: 'center', 82 | }, 83 | textContainer: { 84 | alignItems: 'center', 85 | marginBottom: 48, 86 | }, 87 | text: { 88 | fontSize: fontSizes.xxl, 89 | lineHeight: lineHeights.xxl, 90 | textAlign: 'center', 91 | }, 92 | line1: { 93 | marginTop: 20, 94 | color: theme.text.primary, 95 | }, 96 | line2: { 97 | color: theme.bg.primary, 98 | }, 99 | titleText: { 100 | fontFamily: theme.fonts.introTitle, 101 | }, 102 | subtitleText: { 103 | fontFamily: theme.fonts.introSubtitle, 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /example/src/screens/OnboardingDefault.tsx: -------------------------------------------------------------------------------- 1 | import Onboarding from '@blazejkustra/react-native-onboarding'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 4 | import type { RootStackParamList } from '../App'; 5 | 6 | type Props = NativeStackScreenProps; 7 | 8 | const STORAGE_KEY = 'onboarding_finished'; 9 | 10 | export default function OnboardingDefault({ navigation }: Props) { 11 | return ( 12 | { 67 | await AsyncStorage.setItem(STORAGE_KEY, 'true'); 68 | navigation.goBack(); 69 | }} 70 | onSkip={() => { 71 | navigation.goBack(); 72 | }} 73 | onStepChange={() => {}} 74 | showCloseButton 75 | showBackButton 76 | /> 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /example/src/screens/OnboardingChecklist.tsx: -------------------------------------------------------------------------------- 1 | import Onboarding from '@blazejkustra/react-native-onboarding'; 2 | import { type ImageSourcePropType } from 'react-native'; 3 | import type { OnboardingStep } from '../../../src/spill-onboarding/types'; 4 | 5 | const imageMap: Record = { 6 | create: require('../../assets/checklist/create.png'), 7 | share: require('../../assets/checklist/share.png'), 8 | categorize: require('../../assets/checklist/categorize.png'), 9 | }; 10 | 11 | const theme = { 12 | colors: { 13 | primaryText: '#ffffff', 14 | secondaryText: '#808080', 15 | 16 | accent: '#787671', 17 | secondaryAccent: '#555555', 18 | 19 | button: '#d9d8d4', 20 | buttonText: '#141414', 21 | buttonPurple: '#6F5BFB', 22 | buttonPurpleAccent: '#5928FB', 23 | disabled: '#121212', 24 | shadow: 'rgba(255, 255, 255, 0.1)', 25 | 26 | background: '#1F1F1F', 27 | hoverBackground: '#2A2A2A', 28 | pressedBackground: '#191919', 29 | reverseBackGround: '#E0E0E0', 30 | secondaryBackground: '#323232', 31 | tertiaryBackground: '#3F3F3F', 32 | 33 | violet: '#5F5DFF', 34 | lightPurple: '#7270FF', 35 | purpleAccent: '#A5A3FF', 36 | white: '#ffffff', 37 | }, 38 | } as const; 39 | 40 | export default function OnboardingChecklist() { 41 | const pages = [ 42 | { 43 | key: 'create', 44 | text: 'You can create unlimited lists for any occasion', 45 | title: 'Create Shopping Lists', 46 | }, 47 | { 48 | key: 'share', 49 | text: 'Share and update your lists in real-time with friends and family', 50 | title: 'Share Lists with Others', 51 | }, 52 | { 53 | key: 'categorize', 54 | text: 'Organize items by categories like dairy, vegetables or meat', 55 | title: 'Categorize your Items', 56 | }, 57 | ]; 58 | 59 | const steps: OnboardingStep[] = pages.map((page, index) => ({ 60 | title: page.title, 61 | description: page.text, 62 | buttonLabel: 'Continue', 63 | image: imageMap[page.key] as ImageSourcePropType, 64 | position: index === 0 ? ('top' as const) : ('bottom' as const), 65 | })); 66 | 67 | return ( 68 | {}} 76 | onSkip={() => {}} 77 | showCloseButton={true} 78 | showBackButton={true} 79 | wrapInModalOnWeb={true} 80 | animationDuration={500} 81 | colors={{ 82 | background: { 83 | primary: theme.colors.buttonPurple, 84 | secondary: theme.colors.background, 85 | label: theme.colors.secondaryAccent, 86 | accent: theme.colors.violet, 87 | }, 88 | text: { 89 | primary: theme.colors.primaryText, 90 | secondary: theme.colors.secondaryText, 91 | contrast: theme.colors.white, 92 | }, 93 | }} 94 | /> 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/utils/ThemeContext.tsx: -------------------------------------------------------------------------------- 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 | {children} 82 | ); 83 | } 84 | 85 | export function useTheme() { 86 | return useContext(ThemeContext); 87 | } 88 | -------------------------------------------------------------------------------- /example/src/screens/OnboardingCustomIntro.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Image, StyleSheet, TouchableOpacity } from 'react-native'; 2 | import Onboarding from '@blazejkustra/react-native-onboarding'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 5 | import type { RootStackParamList } from '../App'; 6 | 7 | type Props = NativeStackScreenProps< 8 | RootStackParamList, 9 | 'OnboardingCustomIntro' 10 | >; 11 | 12 | const STORAGE_KEY = 'onboarding_finished'; 13 | 14 | function CustomIntro({ onPressStart }: { onPressStart: () => void }) { 15 | return ( 16 | 17 | 18 | Private AI, Personalized 19 | Tap to begin your journey 20 | 21 | Dive In 22 | 23 | 24 | ); 25 | } 26 | 27 | export default function OnboardingCustomIntro({ navigation }: Props) { 28 | return ( 29 | { 69 | await AsyncStorage.setItem(STORAGE_KEY, 'true'); 70 | navigation.goBack(); 71 | }} 72 | onSkip={() => navigation.goBack()} 73 | onStepChange={() => {}} 74 | showCloseButton 75 | showBackButton={false} 76 | /> 77 | ); 78 | } 79 | 80 | const styles = StyleSheet.create({ 81 | introContainer: { gap: 12, marginTop: 16 }, 82 | logo: { alignSelf: 'flex-start' }, 83 | h1: { fontSize: 24, fontWeight: '800', color: '#E5E7EB' }, 84 | h2: { fontSize: 16, color: 'rgba(229,231,235,0.7)' }, 85 | cta: { 86 | marginTop: 12, 87 | backgroundColor: '#0EA5E9', 88 | paddingVertical: 10, 89 | paddingHorizontal: 14, 90 | borderRadius: 10, 91 | alignSelf: 'flex-start', 92 | }, 93 | ctaText: { color: '#0B1220', fontWeight: '700' }, 94 | }); 95 | -------------------------------------------------------------------------------- /example/src/screens/Home.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { View, Text, Button, StyleSheet } from 'react-native'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 5 | import type { RootStackParamList } from '../App'; 6 | import { useFocusEffect } from '@react-navigation/native'; 7 | 8 | type Props = NativeStackScreenProps; 9 | 10 | const STORAGE_KEY = 'onboarding_finished'; 11 | 12 | export default function Home({ navigation }: Props) { 13 | const [finished, setFinished] = useState(null); 14 | 15 | const load = useCallback(async () => { 16 | try { 17 | const value = await AsyncStorage.getItem(STORAGE_KEY); 18 | setFinished(value === 'true'); 19 | } catch { 20 | setFinished(false); 21 | } 22 | }, []); 23 | 24 | useFocusEffect( 25 | useCallback(() => { 26 | load(); 27 | }, [load]) 28 | ); 29 | 30 | return ( 31 | 32 | React Native Onboarding - Example 33 | 34 | Finished: {finished === null ? 'loading...' : finished ? 'Yes' : 'No'} 35 | 36 | 37 | 38 |