├── .eslintrc.js ├── .github └── workflows │ ├── release.yml │ └── unit-test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── .yarn └── releases │ └── yarn-4.9.1.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── babel.config.js ├── demos ├── example1.gif ├── example2.gif ├── example3.gif └── example4.gif ├── examples ├── example-bare │ ├── .bundle │ │ └── config │ ├── .gitignore │ ├── .watchmanconfig │ ├── App.tsx │ ├── Gemfile │ ├── android │ │ ├── app │ │ │ ├── build.gradle │ │ │ ├── debug.keystore │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── assets │ │ │ │ └── select_click.mp3 │ │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── examplebare │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── MainApplication.kt │ │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ └── values │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle │ ├── app.json │ ├── babel.config.js │ ├── index.js │ ├── ios │ │ ├── .xcode.env │ │ ├── Podfile │ │ ├── exampleBare.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── exampleBare.xcscheme │ │ └── exampleBare │ │ │ ├── AppDelegate.swift │ │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── PrivacyInfo.xcprivacy │ │ │ └── select_click.mp3 │ ├── metro.config.js │ ├── package.json │ ├── tsconfig.json │ └── utils │ │ ├── formatTime.ts │ │ └── getClickSound.ts └── example-expo │ ├── .eslintrc.js │ ├── .gitignore │ ├── App.tsx │ ├── app.json │ ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ ├── select_click.mp3 │ └── splash.png │ ├── babel.config.js │ ├── metro.config.js │ ├── package.json │ ├── tsconfig.json │ └── utils │ ├── formatTime.ts │ └── getClickSound.ts ├── jest.config.js ├── package.json ├── src ├── components │ ├── DurationScroll │ │ ├── DurationScroll.tsx │ │ ├── index.ts │ │ └── types.ts │ ├── Modal │ │ ├── Modal.tsx │ │ ├── index.ts │ │ ├── styles.ts │ │ └── types.ts │ ├── TimerPicker │ │ ├── TimerPicker.tsx │ │ ├── index.ts │ │ ├── styles.ts │ │ └── types.ts │ └── TimerPickerModal │ │ ├── TimerPickerModal.tsx │ │ ├── index.ts │ │ ├── styles.ts │ │ └── types.ts ├── index.ts ├── tests │ ├── DurationScroll.test.tsx │ ├── Modal.test.tsx │ ├── TimerPicker.test.tsx │ ├── TimerPickerModal.test.tsx │ └── __mocks__ │ │ └── expo-linear-gradient.js └── utils │ ├── colorToRgba.ts │ ├── generateNumbers.ts │ ├── getAdjustedLimit.ts │ ├── getDurationAndIndexFromScrollOffset.ts │ ├── getInitialScrollIndex.ts │ ├── getSafeInitialValue.ts │ └── padNumber.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | settings: { 7 | react: { 8 | version: "detect", 9 | }, 10 | }, 11 | extends: [ 12 | "eslint:recommended", 13 | "plugin:react/recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | ], 16 | parser: "@typescript-eslint/parser", 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 12, 22 | sourceType: "module", 23 | }, 24 | plugins: [ 25 | "react", 26 | "react-hooks", 27 | "@typescript-eslint", 28 | "typescript-sort-keys", 29 | "sort-destructure-keys", 30 | "import", 31 | ], 32 | rules: { 33 | "linebreak-style": ["error", "unix"], 34 | quotes: ["warn", "double"], 35 | semi: ["warn", "always"], 36 | "react-hooks/rules-of-hooks": "error", 37 | "react-hooks/exhaustive-deps": "error", 38 | "react/display-name": "off", 39 | "react/prop-types": "off", 40 | "no-unused-vars": "off", // disable the base rule as it can report incorrect errors 41 | "@typescript-eslint/consistent-type-imports": [ 42 | "warn", 43 | { prefer: "type-imports" }, 44 | ], 45 | "@typescript-eslint/no-unused-vars": [ 46 | "warn", 47 | { 48 | vars: "all", 49 | args: "after-used", 50 | ignoreRestSiblings: true, 51 | }, 52 | ], 53 | "no-unreachable": "warn", 54 | "typescript-sort-keys/interface": "warn", 55 | "typescript-sort-keys/string-enum": "warn", 56 | "sort-destructure-keys/sort-destructure-keys": [ 57 | "warn", 58 | { caseSensitive: false }, 59 | ], 60 | "react/jsx-sort-props": [ 61 | "warn", 62 | { 63 | ignoreCase: true, 64 | reservedFirst: ["key", "children", "ref"], 65 | }, 66 | ], 67 | "import/order": [ 68 | "warn", 69 | { 70 | "newlines-between": "always", 71 | distinctGroup: true, 72 | alphabetize: { 73 | order: "asc", 74 | caseInsensitive: true, 75 | orderImportKind: "desc", 76 | }, 77 | groups: [ 78 | "builtin", 79 | "external", 80 | "parent", 81 | "sibling", 82 | "internal", 83 | "unknown", 84 | ], 85 | pathGroupsExcludedImportTypes: ["react"], 86 | pathGroups: [ 87 | { 88 | pattern: "react", 89 | group: "builtin", 90 | position: "before", 91 | }, 92 | { 93 | pattern: "([a-z]|@)**", 94 | group: "external", 95 | }, 96 | { 97 | pattern: "**/styles", 98 | group: "internal", 99 | position: "before", 100 | }, 101 | { 102 | pattern: "**/types", 103 | group: "internal", 104 | position: "before", 105 | }, 106 | { 107 | pattern: "**/components/**", 108 | group: "internal", 109 | }, 110 | { 111 | pattern: "**/utils/**", 112 | group: "internal", 113 | position: "after", 114 | }, 115 | ], 116 | }, 117 | ], 118 | }, 119 | }; -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 release" 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | release: 9 | name: 🚀 release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: checkout 13 | uses: actions/checkout@v1 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 20 18 | registry-url: https://registry.npmjs.org 19 | - name: Install dependencies 20 | run: yarn 21 | - name: Release 22 | run: npm publish 23 | env: 24 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/unit-test.yml: -------------------------------------------------------------------------------- 1 | name: "🧪 unit-test" 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | types: 7 | - opened # When PR is first created 8 | - synchronize # When new commits are pushed to the PR 9 | - reopened # When a closed PR is reopened 10 | 11 | jobs: 12 | release: 13 | name: 🧪 unit-test 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: checkout 17 | uses: actions/checkout@v1 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 20 22 | registry-url: https://registry.npmjs.org 23 | - name: Install react 24 | run: yarn add react@18.2.0 -D 25 | - name: Install react-native 26 | run: yarn add react-native@0.72.0 -D 27 | - name: Install dependencies 28 | run: yarn install 29 | - name: Test 30 | run: yarn test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .expo 3 | dist 4 | *.tgz 5 | .yarn/* 6 | !.yarn/releases 7 | !.yarnrc.yml -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tests/ 3 | node_modules/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "jsxBracketSameLine": true 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": "explicit" 4 | }, 5 | "eslint.workingDirectories": ["./src"], 6 | "gitmoji.addCustomEmoji": [ 7 | { 8 | "emoji": "✨", 9 | "code": ":sparkles:", 10 | "description": "Introduce new features" 11 | }, 12 | { 13 | "emoji": "💄", 14 | "code": ":lipstick:", 15 | "description": "Improve styling/UI" 16 | }, 17 | { 18 | "emoji": "👼", 19 | "code": ":angel:", 20 | "description": "Improve user experience" 21 | }, 22 | { 23 | "emoji": "🧑‍💻", 24 | "code": ":technologist:", 25 | "description": "Improve developer experience" 26 | }, 27 | { 28 | "emoji": "🩹", 29 | "code": ":adhesive_bandage:", 30 | "description": "Minor fix for a non-critical issue" 31 | }, 32 | { 33 | "emoji": "🐛", 34 | "code": ":bug:", 35 | "description": "Fix a bug" 36 | }, 37 | { 38 | "emoji": "🚑️", 39 | "code": ":ambulance:", 40 | "description": "Make a critical patch" 41 | }, 42 | { 43 | "emoji": "😷", 44 | "code": ":mask:", 45 | "description": "Attempt to fix an issue" 46 | }, 47 | { 48 | "emoji": "🦺", 49 | "code": ":safety_vest:", 50 | "description": "Safeguard against an issue" 51 | }, 52 | { 53 | "emoji": "🚧", 54 | "code": ":construction:", 55 | "description": "Work in progress" 56 | }, 57 | { 58 | "emoji": "🧵", 59 | "code": ":thread:", 60 | "description": "Tie up loose end" 61 | }, 62 | { 63 | "emoji": "⚡️", 64 | "code": ":zap:", 65 | "description": "Improve performance" 66 | }, 67 | { 68 | "emoji": "📱", 69 | "code": ":iphone:", 70 | "description": "Adjust styling for different screen sizes" 71 | }, 72 | { 73 | "emoji": "💸", 74 | "code": ":money_with_wings:", 75 | "description": "Make changes to money-related infrastructure" 76 | }, 77 | { 78 | "emoji": "🤖", 79 | "code": ":robot_face:", 80 | "description": "Add or update analytics or tracking code" 81 | }, 82 | { 83 | "emoji": "📝", 84 | "code": ":memo:", 85 | "description": "Add or update documentation or comments" 86 | }, 87 | { 88 | "emoji": "🧽", 89 | "code": ":sponge:", 90 | "description": "Clean up code" 91 | }, 92 | { 93 | "emoji": "🚚", 94 | "code": ":truck:", 95 | "description": "Move or rename files/folders" 96 | }, 97 | { 98 | "emoji": "♻️", 99 | "code": ":recycle:", 100 | "description": "Refactor code" 101 | }, 102 | { 103 | "emoji": "⚰️", 104 | "code": ":coffin:", 105 | "description": "Remove dead code" 106 | }, 107 | { 108 | "emoji": "☠️", 109 | "code": ":skull_and_crossbones:", 110 | "description": "Deprecate code" 111 | }, 112 | { 113 | "emoji": "🍾", 114 | "code": ":champagne:", 115 | "description": "Begin a project" 116 | }, 117 | { 118 | "emoji": "🔖", 119 | "code": ":bookmark:", 120 | "description": "Bump version" 121 | }, 122 | { 123 | "emoji": "🍱", 124 | "code": ":bento:", 125 | "description": "Add or update assets" 126 | }, 127 | { 128 | "emoji": "💬", 129 | "code": ":speech_balloon:", 130 | "description": "Add or update text" 131 | }, 132 | { 133 | "emoji": "➕", 134 | "code": ":heavy_plus_sign:", 135 | "description": "Add a dependency" 136 | }, 137 | { 138 | "emoji": "➖", 139 | "code": ":heavy_minus_sign:", 140 | "description": "Remove a dependency" 141 | }, 142 | { 143 | "emoji": "⬇️", 144 | "code": ":arrow_down:", 145 | "description": "Downgrade dependencies" 146 | }, 147 | { 148 | "emoji": "⬆️", 149 | "code": ":arrow_up:", 150 | "description": "Upgrade dependencies" 151 | }, 152 | { 153 | "emoji": "📌", 154 | "code": ":pushpin:", 155 | "description": "Pin dependencies to specific versions" 156 | }, 157 | { 158 | "emoji": "🤫", 159 | "code": ":shushing_face:", 160 | "description": "Fix linter/compiler warnings" 161 | }, 162 | { 163 | "emoji": "🧪", 164 | "code": ":alembic:", 165 | "description": "Add or update tests" 166 | }, 167 | { 168 | "emoji": "🔨", 169 | "code": ":hammer:", 170 | "description": "Attempt to fix failing test" 171 | }, 172 | { 173 | "emoji": "⚙️", 174 | "code": ":gear:", 175 | "description": "Add or update config" 176 | }, 177 | { 178 | "emoji": "🔐", 179 | "code": ":closed_lock_with_key:", 180 | "description": "Add or update secrets" 181 | }, 182 | { 183 | "emoji": "🗃️", 184 | "code": ":card_file_box:", 185 | "description": "Make database-related changes" 186 | }, 187 | { 188 | "emoji": "🏗️", 189 | "code": ":building_construction:", 190 | "description": "Make architectural/infrastructure changes" 191 | }, 192 | { 193 | "emoji": "🥚", 194 | "code": ":egg:", 195 | "description": "Add or update an easter egg" 196 | } 197 | ], 198 | "gitmoji.onlyUseCustomEmoji": true 199 | } 200 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nmHoistingLimits: workspaces 2 | 3 | nodeLinker: node-modules 4 | 5 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Tim Roberts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:metro-react-native-babel-preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /demos/example1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/demos/example1.gif -------------------------------------------------------------------------------- /demos/example2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/demos/example2.gif -------------------------------------------------------------------------------- /demos/example3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/demos/example3.gif -------------------------------------------------------------------------------- /demos/example4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/demos/example4.gif -------------------------------------------------------------------------------- /examples/example-bare/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /examples/example-bare/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | .kotlin/ 37 | 38 | # node.js 39 | # 40 | node_modules/ 41 | npm-debug.log 42 | yarn-error.log 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | **/fastlane/report.xml 52 | **/fastlane/Preview.html 53 | **/fastlane/screenshots 54 | **/fastlane/test_output 55 | 56 | # Bundle artifact 57 | *.jsbundle 58 | 59 | # Ruby / CocoaPods 60 | **/Pods/ 61 | /vendor/bundle/ 62 | 63 | # Temporary files created by Metro to check the health of the file watcher 64 | .metro-health-check* 65 | 66 | # testing 67 | /coverage 68 | 69 | # Yarn 70 | .yarn/* 71 | !.yarn/patches 72 | !.yarn/plugins 73 | !.yarn/releases 74 | !.yarn/sdks 75 | !.yarn/versions 76 | -------------------------------------------------------------------------------- /examples/example-bare/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /examples/example-bare/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useMemo, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | 9 | import MaskedView from "@react-native-masked-view/masked-view"; 10 | import { 11 | LayoutAnimation, 12 | Platform, 13 | ScrollView, 14 | StyleSheet, 15 | Text, 16 | TouchableOpacity, 17 | UIManager, 18 | View, 19 | useWindowDimensions, 20 | } from "react-native"; 21 | import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"; 22 | import { AudioContext, type AudioBuffer } from "react-native-audio-api"; 23 | import { trigger } from "react-native-haptic-feedback"; 24 | import LinearGradient from "react-native-linear-gradient"; 25 | 26 | import { TimerPicker, TimerPickerModal } from "../../src"; 27 | 28 | import { formatTime } from "./utils/formatTime"; 29 | import { getClickSound } from "./utils/getClickSound"; 30 | 31 | if (Platform.OS === "android") { 32 | UIManager.setLayoutAnimationEnabledExperimental?.(true); 33 | } 34 | 35 | export default function App() { 36 | const { width: screenWidth } = useWindowDimensions(); 37 | 38 | const scrollViewRef = useRef(null); 39 | const audioContextRef = useRef(null); 40 | const audioBufferRef = useRef(null); 41 | 42 | const [currentPageIndex, setCurrentPageIndex] = useState(0); 43 | const [showPickerExample1, setShowPickerExample1] = useState(false); 44 | const [showPickerExample2, setShowPickerExample2] = useState(false); 45 | const [alarmStringExample1, setAlarmStringExample1] = useState< 46 | string | null 47 | >(null); 48 | const [alarmStringExample2, setAlarmStringExample2] = useState< 49 | string | null 50 | >(null); 51 | 52 | useEffect(() => { 53 | const setupAudio = async () => { 54 | try { 55 | const context = new AudioContext(); 56 | const arrayBuffer = await getClickSound(); 57 | const buffer = await context.decodeAudioData(arrayBuffer); 58 | 59 | audioContextRef.current = context; 60 | audioBufferRef.current = buffer; 61 | } catch (error) { 62 | console.warn("Audio setup failed:", error); 63 | } 64 | }; 65 | 66 | setupAudio(); 67 | 68 | return () => { 69 | audioContextRef.current?.close(); 70 | }; 71 | }, []); 72 | 73 | useEffect(() => { 74 | // when changing to landscape mode, scroll to the nearest page index 75 | scrollViewRef.current?.scrollTo({ 76 | x: screenWidth * currentPageIndex, 77 | animated: false, 78 | }); 79 | // eslint-disable-next-line react-hooks/exhaustive-deps 80 | }, [screenWidth]); 81 | 82 | const onMomentumScrollEnd = useCallback( 83 | (event: NativeSyntheticEvent) => { 84 | LayoutAnimation.configureNext( 85 | LayoutAnimation.Presets.easeInEaseOut 86 | ); 87 | const { contentOffset } = event.nativeEvent; 88 | const newPageIndex = Math.round(contentOffset.x / screenWidth) as 89 | | 0 90 | | 1; 91 | setCurrentPageIndex(newPageIndex); 92 | }, 93 | [screenWidth] 94 | ); 95 | 96 | const pickerFeedback = useCallback(() => { 97 | try { 98 | trigger("selection"); 99 | 100 | const context = audioContextRef.current; 101 | const buffer = audioBufferRef.current; 102 | 103 | if (!context || !buffer) { 104 | console.warn("Audio not initialized"); 105 | return; 106 | } 107 | 108 | const playerNode = context.createBufferSource(); 109 | playerNode.buffer = buffer; 110 | playerNode.connect(context.destination); 111 | playerNode.start(context.currentTime); 112 | } catch (error) { 113 | console.warn("Picker feedback failed:", error); 114 | } 115 | }, []); 116 | 117 | const renderExample1 = useMemo(() => { 118 | return ( 119 | 125 | 126 | {alarmStringExample1 !== null 127 | ? "Alarm set for" 128 | : "No alarm set"} 129 | 130 | setShowPickerExample1(true)}> 133 | 134 | {alarmStringExample1 !== null ? ( 135 | 136 | {alarmStringExample1} 137 | 138 | ) : null} 139 | setShowPickerExample1(true)}> 142 | 143 | 145 | {"Set Alarm 🔔"} 146 | 147 | 148 | 149 | 150 | 151 | setShowPickerExample1(false)} 159 | onConfirm={(pickedDuration) => { 160 | setAlarmStringExample1(formatTime(pickedDuration)); 161 | setShowPickerExample1(false); 162 | }} 163 | pickerFeedback={pickerFeedback} 164 | setIsVisible={setShowPickerExample1} 165 | styles={{ 166 | theme: "dark", 167 | }} 168 | visible={showPickerExample1} 169 | /> 170 | 171 | ); 172 | }, [alarmStringExample1, pickerFeedback, screenWidth, showPickerExample1]); 173 | 174 | const renderExample2 = useMemo(() => { 175 | return ( 176 | 182 | 183 | {alarmStringExample2 !== null 184 | ? "Alarm set for" 185 | : "No alarm set"} 186 | 187 | setShowPickerExample2(true)}> 190 | 191 | {alarmStringExample2 !== null ? ( 192 | 193 | {alarmStringExample2} 194 | 195 | ) : null} 196 | setShowPickerExample2(true)}> 199 | 200 | 202 | {"Set Alarm 🔔"} 203 | 204 | 205 | 206 | 207 | 208 | setShowPickerExample2(false)} 213 | onConfirm={(pickedDuration) => { 214 | setAlarmStringExample2(formatTime(pickedDuration)); 215 | setShowPickerExample2(false); 216 | }} 217 | pickerFeedback={pickerFeedback} 218 | setIsVisible={setShowPickerExample2} 219 | styles={{ 220 | theme: "light", 221 | }} 222 | use12HourPicker 223 | visible={showPickerExample2} 224 | /> 225 | 226 | ); 227 | }, [alarmStringExample2, pickerFeedback, screenWidth, showPickerExample2]); 228 | 229 | const renderExample3 = useMemo(() => { 230 | return ( 231 | 240 | 273 | 274 | ); 275 | }, [pickerFeedback, screenWidth]); 276 | 277 | const renderExample4 = useMemo(() => { 278 | return ( 279 | 285 | 309 | 310 | ); 311 | }, [pickerFeedback, screenWidth]); 312 | 313 | return ( 314 | 319 | {renderExample1} 320 | {renderExample2} 321 | {renderExample3} 322 | {renderExample4} 323 | 324 | ); 325 | } 326 | 327 | const styles = StyleSheet.create({ 328 | container: { 329 | alignItems: "center", 330 | justifyContent: "center", 331 | }, 332 | page1Container: { 333 | backgroundColor: "#514242", 334 | }, 335 | page2Container: { 336 | backgroundColor: "#F1F1F1", 337 | }, 338 | page3Container: { 339 | flex: 1, 340 | }, 341 | page4Container: { 342 | backgroundColor: "#F1F1F1", 343 | }, 344 | textDark: { 345 | fontSize: 18, 346 | color: "#F1F1F1", 347 | }, 348 | textLight: { 349 | fontSize: 18, 350 | color: "#202020", 351 | }, 352 | alarmTextDark: { 353 | fontSize: 48, 354 | color: "#F1F1F1", 355 | }, 356 | alarmTextLight: { 357 | fontSize: 48, 358 | color: "#202020", 359 | }, 360 | touchableContainer: { 361 | alignItems: "center", 362 | }, 363 | button: { 364 | paddingVertical: 10, 365 | paddingHorizontal: 18, 366 | borderWidth: 1, 367 | borderRadius: 10, 368 | fontSize: 16, 369 | overflow: "hidden", 370 | }, 371 | buttonDark: { 372 | borderColor: "#C2C2C2", 373 | color: "#C2C2C2", 374 | }, 375 | buttonLight: { borderColor: "#8C8C8C", color: "#8C8C8C" }, 376 | buttonContainer: { 377 | marginTop: 30, 378 | }, 379 | }); 380 | -------------------------------------------------------------------------------- /examples/example-bare/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '< 1.3.4' 11 | 12 | # Ruby 3.4.0 has removed some libraries from the standard library. 13 | gem 'bigdecimal' 14 | gem 'logger' 15 | gem 'benchmark' 16 | gem 'mutex_m' 17 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "org.jetbrains.kotlin.android" 3 | apply plugin: "com.facebook.react" 4 | 5 | /** 6 | * This is the configuration block to customize your React Native Android app. 7 | * By default you don't need to apply any configuration, just uncomment the lines you need. 8 | */ 9 | react { 10 | /* Folders */ 11 | // The root of your project, i.e. where "package.json" lives. Default is '../..' 12 | // root = file("../../") 13 | // The folder where the react-native NPM package is. Default is ../../node_modules/react-native 14 | // reactNativeDir = file("../../node_modules/react-native") 15 | // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen 16 | // codegenDir = file("../../node_modules/@react-native/codegen") 17 | // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js 18 | // cliFile = file("../../node_modules/react-native/cli.js") 19 | 20 | /* Variants */ 21 | // The list of variants to that are debuggable. For those we're going to 22 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 23 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 24 | // debuggableVariants = ["liteDebug", "prodDebug"] 25 | 26 | /* Bundling */ 27 | // A list containing the node command and its flags. Default is just 'node'. 28 | // nodeExecutableAndArgs = ["node"] 29 | // 30 | // The command to run when bundling. By default is 'bundle' 31 | // bundleCommand = "ram-bundle" 32 | // 33 | // The path to the CLI configuration file. Default is empty. 34 | // bundleConfig = file(../rn-cli.config.js) 35 | // 36 | // The name of the generated asset file containing your JS bundle 37 | // bundleAssetName = "MyApplication.android.bundle" 38 | // 39 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 40 | // entryFile = file("../js/MyApplication.android.js") 41 | // 42 | // A list of extra flags to pass to the 'bundle' commands. 43 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 44 | // extraPackagerArgs = [] 45 | 46 | /* Hermes Commands */ 47 | // The hermes compiler command to run. By default it is 'hermesc' 48 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 49 | // 50 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 51 | // hermesFlags = ["-O", "-output-source-map"] 52 | 53 | /* Autolinking */ 54 | autolinkLibrariesWithApp() 55 | } 56 | 57 | /** 58 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 59 | */ 60 | def enableProguardInReleaseBuilds = false 61 | 62 | /** 63 | * The preferred build flavor of JavaScriptCore (JSC) 64 | * 65 | * For example, to use the international variant, you can use: 66 | * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` 67 | * 68 | * The international variant includes ICU i18n library and necessary data 69 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 70 | * give correct results when using with locales other than en-US. Note that 71 | * this variant is about 6MiB larger per architecture than default. 72 | */ 73 | def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' 74 | 75 | android { 76 | ndkVersion rootProject.ext.ndkVersion 77 | buildToolsVersion rootProject.ext.buildToolsVersion 78 | compileSdk rootProject.ext.compileSdkVersion 79 | 80 | namespace "com.examplebare" 81 | defaultConfig { 82 | applicationId "com.examplebare" 83 | minSdkVersion rootProject.ext.minSdkVersion 84 | targetSdkVersion rootProject.ext.targetSdkVersion 85 | versionCode 1 86 | versionName "1.0" 87 | } 88 | signingConfigs { 89 | debug { 90 | storeFile file('debug.keystore') 91 | storePassword 'android' 92 | keyAlias 'androiddebugkey' 93 | keyPassword 'android' 94 | } 95 | } 96 | buildTypes { 97 | debug { 98 | signingConfig signingConfigs.debug 99 | } 100 | release { 101 | // Caution! In production, you need to generate your own keystore file. 102 | // see https://reactnative.dev/docs/signed-apk-android. 103 | signingConfig signingConfigs.debug 104 | minifyEnabled enableProguardInReleaseBuilds 105 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 106 | } 107 | } 108 | } 109 | 110 | dependencies { 111 | // The version of react-native is set by the React Native Gradle Plugin 112 | implementation("com.facebook.react:react-android") 113 | 114 | if (hermesEnabled.toBoolean()) { 115 | implementation("com.facebook.react:hermes-android") 116 | } else { 117 | implementation jscFlavor 118 | } 119 | } -------------------------------------------------------------------------------- /examples/example-bare/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/debug.keystore -------------------------------------------------------------------------------- /examples/example-bare/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/assets/select_click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/assets/select_click.mp3 -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/java/com/examplebare/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.examplebare 2 | 3 | import com.facebook.react.ReactActivity 4 | import com.facebook.react.ReactActivityDelegate 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate 7 | 8 | class MainActivity : ReactActivity() { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | override fun getMainComponentName(): String = "examplebare" 15 | 16 | /** 17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 19 | */ 20 | override fun createReactActivityDelegate(): ReactActivityDelegate = 21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 22 | } 23 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/java/com/examplebare/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.examplebare 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeHost 8 | import com.facebook.react.ReactPackage 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 13 | import com.facebook.soloader.SoLoader 14 | 15 | class MainApplication : Application(), ReactApplication { 16 | 17 | override val reactNativeHost: ReactNativeHost = 18 | object : DefaultReactNativeHost(this) { 19 | override fun getPackages(): List = 20 | PackageList(this).packages.apply { 21 | // Packages that cannot be autolinked yet can be added manually here, for example: 22 | // add(MyReactNativePackage()) 23 | } 24 | 25 | override fun getJSMainModuleName(): String = "index" 26 | 27 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 28 | 29 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 30 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 31 | } 32 | 33 | override val reactHost: ReactHost 34 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 35 | 36 | override fun onCreate() { 37 | super.onCreate() 38 | SoLoader.init(this, OpenSourceMergedSoMapping) 39 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 40 | // If you opted-in for the New Architecture, we load the native entry point for this app. 41 | load() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | examplebare 3 | 4 | -------------------------------------------------------------------------------- /examples/example-bare/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/example-bare/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "35.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 35 6 | targetSdkVersion = 35 7 | ndkVersion = "27.1.12297006" 8 | kotlinVersion = "2.0.21" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle:8.5.0") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /examples/example-bare/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Use this property to specify which architecture you want to build. 26 | # You can also override it from the CLI using 27 | # ./gradlew -PreactNativeArchitectures=x86_64 28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 29 | 30 | # Use this property to enable support to the new architecture. 31 | # This will allow you to use TurboModules and the Fabric render in 32 | # your application. You should enable this flag either if you want 33 | # to write custom TurboModules/Fabric components OR use libraries that 34 | # are providing them. 35 | newArchEnabled=true 36 | 37 | # Use this property to enable or disable the Hermes JS engine. 38 | # If set to false, you will be using JSC instead. 39 | hermesEnabled=true 40 | -------------------------------------------------------------------------------- /examples/example-bare/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /examples/example-bare/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /examples/example-bare/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /examples/example-bare/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /examples/example-bare/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | rootProject.name = 'examplebare' 5 | include ':app' 6 | includeBuild('../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /examples/example-bare/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examplebare", 3 | "slug": "examplebare" 4 | } 5 | -------------------------------------------------------------------------------- /examples/example-bare/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["module:@react-native/babel-preset"], 3 | }; 4 | -------------------------------------------------------------------------------- /examples/example-bare/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from "react-native"; 2 | 3 | import App from "./App"; 4 | import { name as appName } from "./app.json"; 5 | 6 | AppRegistry.registerComponent(appName, () => App); 7 | -------------------------------------------------------------------------------- /examples/example-bare/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /examples/example-bare/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'examplebare' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | # An absolute path to your application root. 23 | :app_path => "#{Pod::Config.instance.installation_root}/.." 24 | ) 25 | 26 | post_install do |installer| 27 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 28 | react_native_post_install( 29 | installer, 30 | config[:reactNativePath], 31 | :mac_catalyst_enabled => false, 32 | # :ccache_enabled => true 33 | ) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0C80B921A6F3F58F76C31292 /* libPods-examplebare.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-examplebare.a */; }; 11 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 12 | 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; 13 | 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXContainerItemProxy section */ 17 | 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { 18 | isa = PBXContainerItemProxy; 19 | containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; 20 | proxyType = 1; 21 | remoteGlobalIDString = 13B07F861A680F5B00A75B9A; 22 | remoteInfo = examplebare; 23 | }; 24 | /* End PBXContainerItemProxy section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 28 | 13B07F961A680F5B00A75B9A /* examplebare.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = examplebare.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = examplebare/Images.xcassets; sourceTree = ""; }; 30 | 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = examplebare/Info.plist; sourceTree = ""; }; 31 | 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = examplebare/PrivacyInfo.xcprivacy; sourceTree = ""; }; 32 | 3B4392A12AC88292D35C810B /* Pods-examplebare.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-examplebare.debug.xcconfig"; path = "Target Support Files/Pods-examplebare/Pods-examplebare.debug.xcconfig"; sourceTree = ""; }; 33 | 5709B34CF0A7D63546082F79 /* Pods-examplebare.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-examplebare.release.xcconfig"; path = "Target Support Files/Pods-examplebare/Pods-examplebare.release.xcconfig"; sourceTree = ""; }; 34 | 5DCACB8F33CDC322A6C60F78 /* libPods-examplebare.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-examplebare.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 35 | 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = examplebare/AppDelegate.swift; sourceTree = ""; }; 36 | 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = examplebare/LaunchScreen.storyboard; sourceTree = ""; }; 37 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 38 | /* End PBXFileReference section */ 39 | 40 | /* Begin PBXFrameworksBuildPhase section */ 41 | 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { 42 | isa = PBXFrameworksBuildPhase; 43 | buildActionMask = 2147483647; 44 | files = ( 45 | 0C80B921A6F3F58F76C31292 /* libPods-examplebare.a in Frameworks */, 46 | ); 47 | runOnlyForDeploymentPostprocessing = 0; 48 | }; 49 | /* End PBXFrameworksBuildPhase section */ 50 | 51 | /* Begin PBXGroup section */ 52 | 00E356F01AD99517003FC87E /* Supporting Files */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 00E356F11AD99517003FC87E /* Info.plist */, 56 | ); 57 | name = "Supporting Files"; 58 | sourceTree = ""; 59 | }; 60 | 13B07FAE1A68108700A75B9A /* examplebare */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 13B07FB51A68108700A75B9A /* Images.xcassets */, 64 | 761780EC2CA45674006654EE /* AppDelegate.swift */, 65 | 13B07FB61A68108700A75B9A /* Info.plist */, 66 | 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 67 | 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, 68 | ); 69 | name = examplebare; 70 | sourceTree = ""; 71 | }; 72 | 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | ED297162215061F000B7C4FE /* JavaScriptCore.framework */, 76 | 5DCACB8F33CDC322A6C60F78 /* libPods-examplebare.a */, 77 | ); 78 | name = Frameworks; 79 | sourceTree = ""; 80 | }; 81 | 832341AE1AAA6A7D00B99B32 /* Libraries */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | ); 85 | name = Libraries; 86 | sourceTree = ""; 87 | }; 88 | 83CBB9F61A601CBA00E9B192 = { 89 | isa = PBXGroup; 90 | children = ( 91 | 13B07FAE1A68108700A75B9A /* examplebare */, 92 | 832341AE1AAA6A7D00B99B32 /* Libraries */, 93 | 83CBBA001A601CBA00E9B192 /* Products */, 94 | 2D16E6871FA4F8E400B85C8A /* Frameworks */, 95 | BBD78D7AC51CEA395F1C20DB /* Pods */, 96 | ); 97 | indentWidth = 2; 98 | sourceTree = ""; 99 | tabWidth = 2; 100 | usesTabs = 0; 101 | }; 102 | 83CBBA001A601CBA00E9B192 /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 13B07F961A680F5B00A75B9A /* examplebare.app */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | BBD78D7AC51CEA395F1C20DB /* Pods */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | 3B4392A12AC88292D35C810B /* Pods-examplebare.debug.xcconfig */, 114 | 5709B34CF0A7D63546082F79 /* Pods-examplebare.release.xcconfig */, 115 | ); 116 | path = Pods; 117 | sourceTree = ""; 118 | }; 119 | /* End PBXGroup section */ 120 | 121 | /* Begin PBXNativeTarget section */ 122 | 13B07F861A680F5B00A75B9A /* examplebare */ = { 123 | isa = PBXNativeTarget; 124 | buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "examplebare" */; 125 | buildPhases = ( 126 | C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, 127 | 13B07F871A680F5B00A75B9A /* Sources */, 128 | 13B07F8C1A680F5B00A75B9A /* Frameworks */, 129 | 13B07F8E1A680F5B00A75B9A /* Resources */, 130 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 131 | 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, 132 | E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, 133 | ); 134 | buildRules = ( 135 | ); 136 | dependencies = ( 137 | ); 138 | name = examplebare; 139 | productName = examplebare; 140 | productReference = 13B07F961A680F5B00A75B9A /* examplebare.app */; 141 | productType = "com.apple.product-type.application"; 142 | }; 143 | /* End PBXNativeTarget section */ 144 | 145 | /* Begin PBXProject section */ 146 | 83CBB9F71A601CBA00E9B192 /* Project object */ = { 147 | isa = PBXProject; 148 | attributes = { 149 | LastUpgradeCheck = 1210; 150 | TargetAttributes = { 151 | 13B07F861A680F5B00A75B9A = { 152 | LastSwiftMigration = 1120; 153 | }; 154 | }; 155 | }; 156 | buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "examplebare" */; 157 | compatibilityVersion = "Xcode 12.0"; 158 | developmentRegion = en; 159 | hasScannedForEncodings = 0; 160 | knownRegions = ( 161 | en, 162 | Base, 163 | ); 164 | mainGroup = 83CBB9F61A601CBA00E9B192; 165 | productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; 166 | projectDirPath = ""; 167 | projectRoot = ""; 168 | targets = ( 169 | 13B07F861A680F5B00A75B9A /* examplebare */, 170 | ); 171 | }; 172 | /* End PBXProject section */ 173 | 174 | /* Begin PBXResourcesBuildPhase section */ 175 | 00E356EC1AD99517003FC87E /* Resources */ = { 176 | isa = PBXResourcesBuildPhase; 177 | buildActionMask = 2147483647; 178 | files = ( 179 | ); 180 | runOnlyForDeploymentPostprocessing = 0; 181 | }; 182 | 13B07F8E1A680F5B00A75B9A /* Resources */ = { 183 | isa = PBXResourcesBuildPhase; 184 | buildActionMask = 2147483647; 185 | files = ( 186 | 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 187 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 188 | ); 189 | runOnlyForDeploymentPostprocessing = 0; 190 | }; 191 | /* End PBXResourcesBuildPhase section */ 192 | 193 | /* Begin PBXShellScriptBuildPhase section */ 194 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { 195 | isa = PBXShellScriptBuildPhase; 196 | buildActionMask = 2147483647; 197 | files = ( 198 | ); 199 | inputPaths = ( 200 | "$(SRCROOT)/.xcode.env.local", 201 | "$(SRCROOT)/.xcode.env", 202 | ); 203 | name = "Bundle React Native code and images"; 204 | outputPaths = ( 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | shellPath = /bin/sh; 208 | shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; 209 | }; 210 | 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { 211 | isa = PBXShellScriptBuildPhase; 212 | buildActionMask = 2147483647; 213 | files = ( 214 | ); 215 | inputFileListPaths = ( 216 | "${PODS_ROOT}/Target Support Files/Pods-examplebare/Pods-examplebare-frameworks-${CONFIGURATION}-input-files.xcfilelist", 217 | ); 218 | name = "[CP] Embed Pods Frameworks"; 219 | outputFileListPaths = ( 220 | "${PODS_ROOT}/Target Support Files/Pods-examplebare/Pods-examplebare-frameworks-${CONFIGURATION}-output-files.xcfilelist", 221 | ); 222 | runOnlyForDeploymentPostprocessing = 0; 223 | shellPath = /bin/sh; 224 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-examplebare/Pods-examplebare-frameworks.sh\"\n"; 225 | showEnvVarsInLog = 0; 226 | }; 227 | C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { 228 | isa = PBXShellScriptBuildPhase; 229 | buildActionMask = 2147483647; 230 | files = ( 231 | ); 232 | inputFileListPaths = ( 233 | ); 234 | inputPaths = ( 235 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 236 | "${PODS_ROOT}/Manifest.lock", 237 | ); 238 | name = "[CP] Check Pods Manifest.lock"; 239 | outputFileListPaths = ( 240 | ); 241 | outputPaths = ( 242 | "$(DERIVED_FILE_DIR)/Pods-examplebare-checkManifestLockResult.txt", 243 | ); 244 | runOnlyForDeploymentPostprocessing = 0; 245 | shellPath = /bin/sh; 246 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 247 | showEnvVarsInLog = 0; 248 | }; 249 | E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { 250 | isa = PBXShellScriptBuildPhase; 251 | buildActionMask = 2147483647; 252 | files = ( 253 | ); 254 | inputFileListPaths = ( 255 | "${PODS_ROOT}/Target Support Files/Pods-examplebare/Pods-examplebare-resources-${CONFIGURATION}-input-files.xcfilelist", 256 | ); 257 | name = "[CP] Copy Pods Resources"; 258 | outputFileListPaths = ( 259 | "${PODS_ROOT}/Target Support Files/Pods-examplebare/Pods-examplebare-resources-${CONFIGURATION}-output-files.xcfilelist", 260 | ); 261 | runOnlyForDeploymentPostprocessing = 0; 262 | shellPath = /bin/sh; 263 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-examplebare/Pods-examplebare-resources.sh\"\n"; 264 | showEnvVarsInLog = 0; 265 | }; 266 | /* End PBXShellScriptBuildPhase section */ 267 | 268 | /* Begin PBXSourcesBuildPhase section */ 269 | 13B07F871A680F5B00A75B9A /* Sources */ = { 270 | isa = PBXSourcesBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, 274 | ); 275 | runOnlyForDeploymentPostprocessing = 0; 276 | }; 277 | /* End PBXSourcesBuildPhase section */ 278 | 279 | /* Begin PBXTargetDependency section */ 280 | 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { 281 | isa = PBXTargetDependency; 282 | target = 13B07F861A680F5B00A75B9A /* examplebare */; 283 | targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; 284 | }; 285 | /* End PBXTargetDependency section */ 286 | 287 | /* Begin XCBuildConfiguration section */ 288 | 13B07F941A680F5B00A75B9A /* Debug */ = { 289 | isa = XCBuildConfiguration; 290 | baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-examplebare.debug.xcconfig */; 291 | buildSettings = { 292 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 293 | CLANG_ENABLE_MODULES = YES; 294 | CURRENT_PROJECT_VERSION = 1; 295 | ENABLE_BITCODE = NO; 296 | INFOPLIST_FILE = examplebare/Info.plist; 297 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 298 | LD_RUNPATH_SEARCH_PATHS = ( 299 | "$(inherited)", 300 | "@executable_path/Frameworks", 301 | ); 302 | MARKETING_VERSION = 1.0; 303 | OTHER_LDFLAGS = ( 304 | "$(inherited)", 305 | "-ObjC", 306 | "-lc++", 307 | ); 308 | PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; 309 | PRODUCT_NAME = examplebare; 310 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 311 | SWIFT_VERSION = 5.0; 312 | VERSIONING_SYSTEM = "apple-generic"; 313 | }; 314 | name = Debug; 315 | }; 316 | 13B07F951A680F5B00A75B9A /* Release */ = { 317 | isa = XCBuildConfiguration; 318 | baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-examplebare.release.xcconfig */; 319 | buildSettings = { 320 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 321 | CLANG_ENABLE_MODULES = YES; 322 | CURRENT_PROJECT_VERSION = 1; 323 | INFOPLIST_FILE = examplebare/Info.plist; 324 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 325 | LD_RUNPATH_SEARCH_PATHS = ( 326 | "$(inherited)", 327 | "@executable_path/Frameworks", 328 | ); 329 | MARKETING_VERSION = 1.0; 330 | OTHER_LDFLAGS = ( 331 | "$(inherited)", 332 | "-ObjC", 333 | "-lc++", 334 | ); 335 | PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; 336 | PRODUCT_NAME = examplebare; 337 | SWIFT_VERSION = 5.0; 338 | VERSIONING_SYSTEM = "apple-generic"; 339 | }; 340 | name = Release; 341 | }; 342 | 83CBBA201A601CBA00E9B192 /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ALWAYS_SEARCH_USER_PATHS = NO; 346 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 347 | CLANG_CXX_LANGUAGE_STANDARD = "c++20"; 348 | CLANG_CXX_LIBRARY = "libc++"; 349 | CLANG_ENABLE_MODULES = YES; 350 | CLANG_ENABLE_OBJC_ARC = YES; 351 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 352 | CLANG_WARN_BOOL_CONVERSION = YES; 353 | CLANG_WARN_COMMA = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 356 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 357 | CLANG_WARN_EMPTY_BODY = YES; 358 | CLANG_WARN_ENUM_CONVERSION = YES; 359 | CLANG_WARN_INFINITE_RECURSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 363 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 364 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 365 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 366 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 367 | CLANG_WARN_STRICT_PROTOTYPES = YES; 368 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 369 | CLANG_WARN_UNREACHABLE_CODE = YES; 370 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 371 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 372 | COPY_PHASE_STRIP = NO; 373 | ENABLE_STRICT_OBJC_MSGSEND = YES; 374 | ENABLE_TESTABILITY = YES; 375 | "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; 376 | GCC_C_LANGUAGE_STANDARD = gnu99; 377 | GCC_DYNAMIC_NO_PIC = NO; 378 | GCC_NO_COMMON_BLOCKS = YES; 379 | GCC_OPTIMIZATION_LEVEL = 0; 380 | GCC_PREPROCESSOR_DEFINITIONS = ( 381 | "DEBUG=1", 382 | "$(inherited)", 383 | ); 384 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 387 | GCC_WARN_UNDECLARED_SELECTOR = YES; 388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 389 | GCC_WARN_UNUSED_FUNCTION = YES; 390 | GCC_WARN_UNUSED_VARIABLE = YES; 391 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 392 | LD_RUNPATH_SEARCH_PATHS = ( 393 | /usr/lib/swift, 394 | "$(inherited)", 395 | ); 396 | LIBRARY_SEARCH_PATHS = ( 397 | "\"$(SDKROOT)/usr/lib/swift\"", 398 | "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", 399 | "\"$(inherited)\"", 400 | ); 401 | MTL_ENABLE_DEBUG_INFO = YES; 402 | ONLY_ACTIVE_ARCH = YES; 403 | OTHER_CPLUSPLUSFLAGS = ( 404 | "$(OTHER_CFLAGS)", 405 | "-DFOLLY_NO_CONFIG", 406 | "-DFOLLY_MOBILE=1", 407 | "-DFOLLY_USE_LIBCPP=1", 408 | "-DFOLLY_CFG_NO_COROUTINES=1", 409 | "-DFOLLY_HAVE_CLOCK_GETTIME=1", 410 | ); 411 | SDKROOT = iphoneos; 412 | }; 413 | name = Debug; 414 | }; 415 | 83CBBA211A601CBA00E9B192 /* Release */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ALWAYS_SEARCH_USER_PATHS = NO; 419 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 420 | CLANG_CXX_LANGUAGE_STANDARD = "c++20"; 421 | CLANG_CXX_LIBRARY = "libc++"; 422 | CLANG_ENABLE_MODULES = YES; 423 | CLANG_ENABLE_OBJC_ARC = YES; 424 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 425 | CLANG_WARN_BOOL_CONVERSION = YES; 426 | CLANG_WARN_COMMA = YES; 427 | CLANG_WARN_CONSTANT_CONVERSION = YES; 428 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 429 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 430 | CLANG_WARN_EMPTY_BODY = YES; 431 | CLANG_WARN_ENUM_CONVERSION = YES; 432 | CLANG_WARN_INFINITE_RECURSION = YES; 433 | CLANG_WARN_INT_CONVERSION = YES; 434 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 435 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 436 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 437 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 438 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 439 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 440 | CLANG_WARN_STRICT_PROTOTYPES = YES; 441 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 442 | CLANG_WARN_UNREACHABLE_CODE = YES; 443 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 444 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 445 | COPY_PHASE_STRIP = YES; 446 | ENABLE_NS_ASSERTIONS = NO; 447 | ENABLE_STRICT_OBJC_MSGSEND = YES; 448 | "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; 449 | GCC_C_LANGUAGE_STANDARD = gnu99; 450 | GCC_NO_COMMON_BLOCKS = YES; 451 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 452 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 453 | GCC_WARN_UNDECLARED_SELECTOR = YES; 454 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 455 | GCC_WARN_UNUSED_FUNCTION = YES; 456 | GCC_WARN_UNUSED_VARIABLE = YES; 457 | IPHONEOS_DEPLOYMENT_TARGET = 15.1; 458 | LD_RUNPATH_SEARCH_PATHS = ( 459 | /usr/lib/swift, 460 | "$(inherited)", 461 | ); 462 | LIBRARY_SEARCH_PATHS = ( 463 | "\"$(SDKROOT)/usr/lib/swift\"", 464 | "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", 465 | "\"$(inherited)\"", 466 | ); 467 | MTL_ENABLE_DEBUG_INFO = NO; 468 | OTHER_CPLUSPLUSFLAGS = ( 469 | "$(OTHER_CFLAGS)", 470 | "-DFOLLY_NO_CONFIG", 471 | "-DFOLLY_MOBILE=1", 472 | "-DFOLLY_USE_LIBCPP=1", 473 | "-DFOLLY_CFG_NO_COROUTINES=1", 474 | "-DFOLLY_HAVE_CLOCK_GETTIME=1", 475 | ); 476 | SDKROOT = iphoneos; 477 | VALIDATE_PRODUCT = YES; 478 | }; 479 | name = Release; 480 | }; 481 | /* End XCBuildConfiguration section */ 482 | 483 | /* Begin XCConfigurationList section */ 484 | 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "examplebare" */ = { 485 | isa = XCConfigurationList; 486 | buildConfigurations = ( 487 | 13B07F941A680F5B00A75B9A /* Debug */, 488 | 13B07F951A680F5B00A75B9A /* Release */, 489 | ); 490 | defaultConfigurationIsVisible = 0; 491 | defaultConfigurationName = Release; 492 | }; 493 | 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "examplebare" */ = { 494 | isa = XCConfigurationList; 495 | buildConfigurations = ( 496 | 83CBBA201A601CBA00E9B192 /* Debug */, 497 | 83CBBA211A601CBA00E9B192 /* Release */, 498 | ); 499 | defaultConfigurationIsVisible = 0; 500 | defaultConfigurationName = Release; 501 | }; 502 | /* End XCConfigurationList section */ 503 | }; 504 | rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; 505 | } 506 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare.xcodeproj/xcshareddata/xcschemes/exampleBare.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 35 | 37 | 43 | 44 | 45 | 46 | 52 | 54 | 60 | 61 | 62 | 63 | 65 | 66 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import React 3 | import React_RCTAppDelegate 4 | import ReactAppDependencyProvider 5 | 6 | @main 7 | class AppDelegate: UIResponder, UIApplicationDelegate { 8 | var window: UIWindow? 9 | 10 | var reactNativeDelegate: ReactNativeDelegate? 11 | var reactNativeFactory: RCTReactNativeFactory? 12 | 13 | func application( 14 | _ application: UIApplication, 15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 16 | ) -> Bool { 17 | let delegate = ReactNativeDelegate() 18 | let factory = RCTReactNativeFactory(delegate: delegate) 19 | delegate.dependencyProvider = RCTAppDependencyProvider() 20 | 21 | reactNativeDelegate = delegate 22 | reactNativeFactory = factory 23 | 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | 26 | factory.startReactNative( 27 | withModuleName: "examplebare", 28 | in: window, 29 | launchOptions: launchOptions 30 | ) 31 | 32 | return true 33 | } 34 | } 35 | 36 | class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { 37 | override func sourceURL(for bridge: RCTBridge) -> URL? { 38 | self.bundleURL() 39 | } 40 | 41 | override func bundleURL() -> URL? { 42 | #if DEBUG 43 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") 44 | #else 45 | Bundle.main.url(forResource: "main", withExtension: "jsbundle") 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | examplebare 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | 30 | NSAllowsArbitraryLoads 31 | 32 | NSAllowsLocalNetworking 33 | 34 | 35 | NSLocationWhenInUseUsageDescription 36 | 37 | UILaunchStoryboardName 38 | LaunchScreen 39 | UIRequiredDeviceCapabilities 40 | 41 | arm64 42 | 43 | UISupportedInterfaceOrientations 44 | 45 | UIInterfaceOrientationPortrait 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | UIViewControllerBasedStatusBarAppearance 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/example-bare/ios/exampleBare/select_click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-bare/ios/exampleBare/select_click.mp3 -------------------------------------------------------------------------------- /examples/example-bare/metro.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require("path"); 3 | 4 | const { getDefaultConfig } = require("@react-native/metro-config"); 5 | 6 | const config = getDefaultConfig(__dirname); 7 | 8 | config.transformer.getTransformOptions = async () => ({ 9 | transform: { 10 | experimentalImportSupport: false, 11 | inlineRequires: false, 12 | }, 13 | }); 14 | 15 | const extraNodeModules = { 16 | "react-native-timer-picker": path.resolve(__dirname, "../../src"), 17 | }; 18 | 19 | config.resolver.extraNodeModules = new Proxy(extraNodeModules, { 20 | get: (target, name) => 21 | // redirects dependencies referenced from src/ to local node_modules 22 | name in target 23 | ? target[name] 24 | : path.join(process.cwd(), `node_modules/${name}`), 25 | }); 26 | 27 | config.watchFolders = [path.resolve(__dirname, "../../src")]; 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /examples/example-bare/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-bare", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "lint": "eslint .", 9 | "start": "react-native start" 10 | }, 11 | "dependencies": { 12 | "@react-native-masked-view/masked-view": "0.3.2", 13 | "react": "19.0.0", 14 | "react-native": "0.79.2", 15 | "react-native-audio-api": "0.6.0", 16 | "react-native-fs": "2.20.0", 17 | "react-native-haptic-feedback": "2.3.3", 18 | "react-native-linear-gradient": "2.8.3" 19 | }, 20 | "devDependencies": { 21 | "@babel/preset-env": "^7.25.3", 22 | "@babel/runtime": "^7.25.0", 23 | "@react-native-community/cli": "18.0.0", 24 | "@react-native-community/cli-platform-android": "18.0.0", 25 | "@react-native-community/cli-platform-ios": "18.0.0", 26 | "@react-native/babel-preset": "0.79.2", 27 | "@react-native/eslint-config": "0.79.2", 28 | "@react-native/metro-config": "0.79.2", 29 | "@react-native/typescript-config": "0.79.2" 30 | }, 31 | "engines": { 32 | "node": ">=18" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/example-bare/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true, 7 | "paths": { 8 | "react": ["./node_modules/@types/react"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-bare/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats time components into a string representation of time in HH:MM:SS format. 3 | * Each component is optional and will only be included if provided. 4 | * All components are padded with leading zeros to ensure consistent formatting. 5 | * 6 | * @param {Object} params - The time components to format 7 | * @param {number} [params.hours] - Hours component (0-23) 8 | * @param {number} [params.minutes] - Minutes component (0-59) 9 | * @param {number} [params.seconds] - Seconds component (0-59) 10 | * @returns {string} Formatted time string in HH:MM:SS format, with only provided components included 11 | * 12 | * @example 13 | * formatTime({ hours: 1, minutes: 30, seconds: 45 }) // returns "01:30:45" 14 | * formatTime({ minutes: 5, seconds: 9 }) // returns "05:09" 15 | * formatTime({ hours: 23 }) // returns "23" 16 | */ 17 | export const formatTime = ({ 18 | hours, 19 | minutes, 20 | seconds, 21 | }: { 22 | hours?: number; 23 | minutes?: number; 24 | seconds?: number; 25 | }): string => { 26 | const timeParts = []; 27 | 28 | if (hours !== undefined) { 29 | timeParts.push(hours.toString().padStart(2, "0")); 30 | } 31 | if (minutes !== undefined) { 32 | timeParts.push(minutes.toString().padStart(2, "0")); 33 | } 34 | if (seconds !== undefined) { 35 | timeParts.push(seconds.toString().padStart(2, "0")); 36 | } 37 | 38 | return timeParts.join(":"); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/example-bare/utils/getClickSound.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import RNFS from "react-native-fs"; 3 | 4 | /** 5 | * Retrieves and processes a click sound file for use in the application. 6 | * The function handles platform-specific file paths and converts the sound file 7 | * into an ArrayBuffer format suitable for audio playback. 8 | * 9 | * For Android: Reads from android/app/src/main/assets/select_click.mp3 10 | * For iOS: Reads from the main bundle path/select_click.mp3 11 | * 12 | * @returns {Promise} A promise that resolves to an ArrayBuffer containing the sound file data 13 | * @throws {Error} If the file cannot be read or if the platform is not supported 14 | * 15 | * @example 16 | * const soundBuffer = await getClickSound(); 17 | * // Use soundBuffer with AudioContext or other audio APIs 18 | */ 19 | export const getClickSound = async () => { 20 | let fileData: string; 21 | if (Platform.OS === "android") { 22 | // this reads from android/app/src/main/assets - place your click sound in this folder 23 | fileData = await RNFS.readFileAssets("select_click.mp3", "base64"); 24 | } else { 25 | // this reads from the iOS project - add your file to the project on xcode 26 | const filePath = `${RNFS.MainBundlePath}/select_click.mp3`; 27 | fileData = await RNFS.readFile(filePath, "base64"); 28 | } 29 | 30 | const binaryString = atob(fileData); // Base64 to binary 31 | const len = binaryString.length; 32 | const bytes = new Uint8Array(len); 33 | for (let i = 0; i < len; i++) { 34 | bytes[i] = binaryString.charCodeAt(i); 35 | } 36 | 37 | return bytes.buffer; 38 | }; 39 | -------------------------------------------------------------------------------- /examples/example-expo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("../../.eslintrc.js"); -------------------------------------------------------------------------------- /examples/example-expo/.gitignore: -------------------------------------------------------------------------------- 1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files 2 | 3 | # dependencies 4 | node_modules/ 5 | 6 | # Expo 7 | .expo/ 8 | dist/ 9 | web-build/ 10 | 11 | # Native 12 | *.orig.* 13 | *.jks 14 | *.p8 15 | *.p12 16 | *.key 17 | *.mobileprovision 18 | android/ 19 | 20 | # Metro 21 | .metro-health-check* 22 | 23 | # debug 24 | npm-debug.* 25 | yarn-debug.* 26 | yarn-error.* 27 | 28 | # macOS 29 | .DS_Store 30 | *.pem 31 | 32 | # local env files 33 | .env*.local 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | -------------------------------------------------------------------------------- /examples/example-expo/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useMemo, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | 9 | import Ionicons from "@expo/vector-icons/Ionicons"; 10 | import MaskedView from "@react-native-masked-view/masked-view"; 11 | import * as Haptics from "expo-haptics"; 12 | import { LinearGradient } from "expo-linear-gradient"; 13 | import { 14 | LayoutAnimation, 15 | Platform, 16 | Pressable, 17 | ScrollView, 18 | StyleSheet, 19 | Text, 20 | TouchableOpacity, 21 | UIManager, 22 | View, 23 | useWindowDimensions, 24 | } from "react-native"; 25 | import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native"; 26 | // import { AudioContext, type AudioBuffer } from "react-native-audio-api"; 27 | 28 | import { TimerPicker, TimerPickerModal } from "../../src"; 29 | 30 | import { formatTime } from "./utils/formatTime"; 31 | // import { getClickSound } from "./utils/getClickSound"; 32 | 33 | if (Platform.OS === "android") { 34 | UIManager.setLayoutAnimationEnabledExperimental?.(true); 35 | } 36 | 37 | export default function App() { 38 | const { width: screenWidth } = useWindowDimensions(); 39 | 40 | const scrollViewRef = useRef(null); 41 | // const audioContextRef = useRef(null); 42 | // const audioBufferRef = useRef(null); 43 | 44 | const [currentPageIndex, setCurrentPageIndex] = useState(0); 45 | const [showPickerExample1, setShowPickerExample1] = useState(false); 46 | const [showPickerExample2, setShowPickerExample2] = useState(false); 47 | const [alarmStringExample1, setAlarmStringExample1] = useState< 48 | string | null 49 | >(null); 50 | const [alarmStringExample2, setAlarmStringExample2] = useState< 51 | string | null 52 | >(null); 53 | 54 | // N.B. Uncomment this to use audio (requires development build) 55 | // useEffect(() => { 56 | // const setupAudio = async () => { 57 | // try { 58 | // const context = new AudioContext(); 59 | // const arrayBuffer = await getClickSound(); 60 | // const buffer = await context.decodeAudioData(arrayBuffer); 61 | 62 | // audioContextRef.current = context; 63 | // audioBufferRef.current = buffer; 64 | // } catch (error) { 65 | // console.warn("Audio setup failed:", error); 66 | // } 67 | // }; 68 | 69 | // setupAudio(); 70 | 71 | // return () => { 72 | // audioContextRef.current?.close(); 73 | // }; 74 | // }, []); 75 | 76 | useEffect(() => { 77 | // when changing to landscape mode, scroll to the nearest page index 78 | scrollViewRef.current?.scrollTo({ 79 | x: screenWidth * currentPageIndex, 80 | animated: false, 81 | }); 82 | // eslint-disable-next-line react-hooks/exhaustive-deps 83 | }, [screenWidth]); 84 | 85 | const onMomentumScrollEnd = useCallback( 86 | (event: NativeSyntheticEvent) => { 87 | LayoutAnimation.configureNext( 88 | LayoutAnimation.Presets.easeInEaseOut 89 | ); 90 | const { contentOffset } = event.nativeEvent; 91 | const newPageIndex = Math.round(contentOffset.x / screenWidth) as 92 | | 0 93 | | 1; 94 | setCurrentPageIndex(newPageIndex); 95 | }, 96 | [screenWidth] 97 | ); 98 | 99 | const pickerFeedback = useCallback(() => { 100 | try { 101 | Haptics.selectionAsync(); 102 | 103 | // const context = audioContextRef.current; 104 | // const buffer = audioBufferRef.current; 105 | 106 | // if (!context || !buffer) { 107 | // console.warn("Audio not initialized"); 108 | // return; 109 | // } 110 | 111 | // const playerNode = context.createBufferSource(); 112 | // playerNode.buffer = buffer; 113 | // playerNode.connect(context.destination); 114 | // playerNode.start(context.currentTime); 115 | } catch (error) { 116 | console.warn("Picker feedback failed:", error); 117 | } 118 | }, []); 119 | 120 | const renderExample1 = useMemo(() => { 121 | return ( 122 | 128 | 129 | {alarmStringExample1 !== null 130 | ? "Alarm set for" 131 | : "No alarm set"} 132 | 133 | setShowPickerExample1(true)}> 136 | 137 | {alarmStringExample1 !== null ? ( 138 | 139 | {alarmStringExample1} 140 | 141 | ) : null} 142 | setShowPickerExample1(true)}> 145 | 146 | 148 | {"Set Alarm 🔔"} 149 | 150 | 151 | 152 | 153 | 154 | setShowPickerExample1(false)} 162 | onConfirm={(pickedDuration) => { 163 | setAlarmStringExample1(formatTime(pickedDuration)); 164 | setShowPickerExample1(false); 165 | }} 166 | pickerFeedback={pickerFeedback} 167 | setIsVisible={setShowPickerExample1} 168 | styles={{ 169 | theme: "dark", 170 | }} 171 | visible={showPickerExample1} 172 | /> 173 | 174 | ); 175 | }, [alarmStringExample1, pickerFeedback, screenWidth, showPickerExample1]); 176 | 177 | const renderExample2 = useMemo(() => { 178 | return ( 179 | 185 | 186 | {alarmStringExample2 !== null 187 | ? "Alarm set for" 188 | : "No alarm set"} 189 | 190 | setShowPickerExample2(true)}> 193 | 194 | {alarmStringExample2 !== null ? ( 195 | 196 | {alarmStringExample2} 197 | 198 | ) : null} 199 | setShowPickerExample2(true)}> 202 | 203 | 205 | {"Set Alarm 🔔"} 206 | 207 | 208 | 209 | 210 | 211 | setShowPickerExample2(false)} 216 | onConfirm={(pickedDuration) => { 217 | setAlarmStringExample2(formatTime(pickedDuration)); 218 | setShowPickerExample2(false); 219 | }} 220 | pickerFeedback={pickerFeedback} 221 | setIsVisible={setShowPickerExample2} 222 | styles={{ 223 | theme: "light", 224 | }} 225 | use12HourPicker 226 | visible={showPickerExample2} 227 | /> 228 | 229 | ); 230 | }, [alarmStringExample2, pickerFeedback, screenWidth, showPickerExample2]); 231 | 232 | const renderExample3 = useMemo(() => { 233 | return ( 234 | 243 | 276 | 277 | ); 278 | }, [pickerFeedback, screenWidth]); 279 | 280 | const renderExample4 = useMemo(() => { 281 | return ( 282 | 288 | 312 | 313 | ); 314 | }, [pickerFeedback, screenWidth]); 315 | 316 | const renderNavigationArrows = useMemo(() => { 317 | return ( 318 | <> 319 | {currentPageIndex !== 3 ? ( 320 | { 322 | LayoutAnimation.configureNext( 323 | LayoutAnimation.Presets.easeInEaseOut 324 | ); 325 | setCurrentPageIndex((currentPageIndex) => { 326 | scrollViewRef.current?.scrollTo({ 327 | x: screenWidth * (currentPageIndex + 1), 328 | animated: true, 329 | }); 330 | return currentPageIndex + 1; 331 | }); 332 | }} 333 | style={({ pressed }) => [ 334 | styles.chevronPressable, 335 | { right: 8 }, 336 | pressed && styles.chevronPressable_pressed, 337 | ]}> 338 | 347 | 348 | ) : null} 349 | {currentPageIndex !== 0 ? ( 350 | { 352 | LayoutAnimation.configureNext( 353 | LayoutAnimation.Presets.easeInEaseOut 354 | ); 355 | setCurrentPageIndex((currentPageIndex) => { 356 | scrollViewRef.current?.scrollTo({ 357 | x: screenWidth * (currentPageIndex - 1), 358 | animated: true, 359 | }); 360 | return currentPageIndex - 1; 361 | }); 362 | }} 363 | style={({ pressed }) => [ 364 | styles.chevronPressable, 365 | { left: 8 }, 366 | pressed && styles.chevronPressable_pressed, 367 | ]}> 368 | 377 | 378 | ) : null} 379 | 380 | ); 381 | }, [currentPageIndex, screenWidth]); 382 | 383 | return ( 384 | <> 385 | 390 | {renderExample1} 391 | {renderExample2} 392 | {renderExample3} 393 | {renderExample4} 394 | 395 | {renderNavigationArrows} 396 | 397 | ); 398 | } 399 | 400 | const styles = StyleSheet.create({ 401 | container: { 402 | alignItems: "center", 403 | justifyContent: "center", 404 | }, 405 | page1Container: { 406 | backgroundColor: "#514242", 407 | }, 408 | page2Container: { 409 | backgroundColor: "#F1F1F1", 410 | }, 411 | page3Container: { 412 | flex: 1, 413 | }, 414 | page4Container: { 415 | backgroundColor: "#F1F1F1", 416 | }, 417 | textDark: { 418 | fontSize: 18, 419 | color: "#F1F1F1", 420 | }, 421 | textLight: { 422 | fontSize: 18, 423 | color: "#202020", 424 | }, 425 | alarmTextDark: { 426 | fontSize: 48, 427 | color: "#F1F1F1", 428 | }, 429 | alarmTextLight: { 430 | fontSize: 48, 431 | color: "#202020", 432 | }, 433 | touchableContainer: { 434 | alignItems: "center", 435 | }, 436 | button: { 437 | paddingVertical: 10, 438 | paddingHorizontal: 18, 439 | borderWidth: 1, 440 | borderRadius: 10, 441 | fontSize: 16, 442 | overflow: "hidden", 443 | }, 444 | buttonDark: { 445 | borderColor: "#C2C2C2", 446 | color: "#C2C2C2", 447 | }, 448 | buttonLight: { borderColor: "#8C8C8C", color: "#8C8C8C" }, 449 | buttonContainer: { 450 | marginTop: 30, 451 | }, 452 | chevronPressable: { 453 | justifyContent: "center", 454 | alignItems: "center", 455 | position: "absolute", 456 | top: 0, 457 | bottom: 0, 458 | padding: 8, 459 | }, 460 | chevronPressable_pressed: { 461 | opacity: 0.7, 462 | }, 463 | }); 464 | -------------------------------------------------------------------------------- /examples/example-expo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Timer Picker Example", 4 | "slug": "timer-picker-example", 5 | "version": "1.0.0", 6 | "icon": "./assets/icon.png", 7 | "newArchEnabled": true, 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "assetBundlePatterns": [ 14 | "**/*" 15 | ], 16 | "ios": { 17 | "supportsTablet": true 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./assets/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | }, 24 | "edgeToEdgeEnabled": true, 25 | "package": "com.nuumi.timerpickerexample" 26 | }, 27 | "web": { 28 | "favicon": "./assets/favicon.png" 29 | }, 30 | "plugins": [ 31 | "expo-asset", 32 | "expo-font" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/example-expo/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-expo/assets/adaptive-icon.png -------------------------------------------------------------------------------- /examples/example-expo/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-expo/assets/favicon.png -------------------------------------------------------------------------------- /examples/example-expo/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-expo/assets/icon.png -------------------------------------------------------------------------------- /examples/example-expo/assets/select_click.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-expo/assets/select_click.mp3 -------------------------------------------------------------------------------- /examples/example-expo/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/troberts-28/react-native-timer-picker/37fd922551abb1690d8c6d33e087da9accfc9aa0/examples/example-expo/assets/splash.png -------------------------------------------------------------------------------- /examples/example-expo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/example-expo/metro.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require("path"); 3 | 4 | const { getDefaultConfig } = require("expo/metro-config"); 5 | 6 | const config = getDefaultConfig(__dirname); 7 | 8 | config.transformer.getTransformOptions = async () => ({ 9 | transform: { 10 | experimentalImportSupport: false, 11 | inlineRequires: false, 12 | }, 13 | }); 14 | 15 | const extraNodeModules = { 16 | "react-native-timer-picker": path.resolve(__dirname, "../../src"), 17 | }; 18 | 19 | config.resolver.extraNodeModules = new Proxy(extraNodeModules, { 20 | get: (target, name) => 21 | // redirects dependencies referenced from src/ to local node_modules 22 | name in target 23 | ? target[name] 24 | : path.join(process.cwd(), `node_modules/${name}`), 25 | }); 26 | 27 | config.watchFolders = [path.resolve(__dirname, "../../src")]; 28 | 29 | module.exports = config; 30 | -------------------------------------------------------------------------------- /examples/example-expo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-expo", 3 | "version": "1.0.0", 4 | "main": "node_modules/expo/AppEntry.js", 5 | "scripts": { 6 | "start": "npx expo start", 7 | "android": "npx expo start --android", 8 | "ios": "npx expo start --ios", 9 | "web": "npx expo start --web", 10 | "build:android": "npx expo run:android", 11 | "build:ios": "npx expo run:ios" 12 | }, 13 | "dependencies": { 14 | "@expo/vector-icons": "^14.1.0", 15 | "@react-native-masked-view/masked-view": "0.3.2", 16 | "expo": "^53.0.0", 17 | "expo-asset": "~11.1.5", 18 | "expo-av": "~15.1.4", 19 | "expo-font": "~13.3.1", 20 | "expo-haptics": "~14.1.4", 21 | "expo-linear-gradient": "~14.1.4", 22 | "react": "19.0.0", 23 | "react-native": "0.79.2", 24 | "react-native-audio-api": "0.6.0" 25 | }, 26 | "private": true 27 | } 28 | -------------------------------------------------------------------------------- /examples/example-expo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true, 7 | "paths": { 8 | "react": ["./node_modules/@types/react"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/example-expo/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats time components into a string representation of time in HH:MM:SS format. 3 | * Each component is optional and will only be included if provided. 4 | * All components are padded with leading zeros to ensure consistent formatting. 5 | * 6 | * @param {Object} params - The time components to format 7 | * @param {number} [params.hours] - Hours component (0-23) 8 | * @param {number} [params.minutes] - Minutes component (0-59) 9 | * @param {number} [params.seconds] - Seconds component (0-59) 10 | * @returns {string} Formatted time string in HH:MM:SS format, with only provided components included 11 | * 12 | * @example 13 | * formatTime({ hours: 1, minutes: 30, seconds: 45 }) // returns "01:30:45" 14 | * formatTime({ minutes: 5, seconds: 9 }) // returns "05:09" 15 | * formatTime({ hours: 23 }) // returns "23" 16 | */ 17 | export const formatTime = ({ 18 | hours, 19 | minutes, 20 | seconds, 21 | }: { 22 | hours?: number; 23 | minutes?: number; 24 | seconds?: number; 25 | }): string => { 26 | const timeParts = []; 27 | 28 | if (hours !== undefined) { 29 | timeParts.push(hours.toString().padStart(2, "0")); 30 | } 31 | if (minutes !== undefined) { 32 | timeParts.push(minutes.toString().padStart(2, "0")); 33 | } 34 | if (seconds !== undefined) { 35 | timeParts.push(seconds.toString().padStart(2, "0")); 36 | } 37 | 38 | return timeParts.join(":"); 39 | }; 40 | -------------------------------------------------------------------------------- /examples/example-expo/utils/getClickSound.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "expo-asset"; 2 | 3 | /** 4 | * Loads and returns the click sound effect as an ArrayBuffer. 5 | * This function loads a sound file from the assets directory and converts it to an ArrayBuffer 6 | * that can be used for audio playback. 7 | * 8 | * @returns {Promise} A promise that resolves to the sound file as an ArrayBuffer 9 | * @throws {Error} If the asset fails to load or if the fetch request fails 10 | * 11 | * @example 12 | * const clickSound = await getClickSound(); 13 | * // Use clickSound with Audio API or other audio playback methods 14 | */ 15 | export const getClickSound = async () => { 16 | const [asset] = await Asset.loadAsync( 17 | // eslint-disable-next-line @typescript-eslint/no-var-requires 18 | require("../assets/select_click.mp3") 19 | ); 20 | 21 | const response = await fetch(asset.uri); 22 | return await response.arrayBuffer(); 23 | }; 24 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "react-native", 3 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 4 | testEnvironment: "node", 5 | modulePathIgnorePatterns: ["/dist/", "/examples/"], 6 | transformIgnorePatterns: [ 7 | "node_modules/(?!(react-native|@react-native|@react-native-community|@react-navigation|react-clone-referenced-element|@react-native-picker)/)", 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-timer-picker", 3 | "description": "A simple, flexible, performant duration picker for React Native apps 🔥\n\nGreat for timers, alarms and duration inputs ⏰🕰️⏳\n\nIncludes iOS-style haptic and audio feedback 🍏", 4 | "author": { 5 | "name": "Tim Roberts", 6 | "url": "https://github.com/troberts-28" 7 | }, 8 | "license": "MIT", 9 | "version": "2.2.0", 10 | "main": "dist/commonjs/index.js", 11 | "module": "dist/module/index.js", 12 | "types": "dist/typescript/index.d.ts", 13 | "typings": "dist/typescript/index.d.ts", 14 | "packageManager": "yarn@4.9.1", 15 | "workspaces": { 16 | "packages": [ 17 | "examples/*" 18 | ] 19 | }, 20 | "scripts": { 21 | "setup": "yarn install", 22 | "start": " yarn workspace example-expo start", 23 | "start-bare:android": "yarn workspace example-bare android && yarn workspace example-bare start", 24 | "start-bare:ios": "yarn workspace example-bare ios && yarn workspace example-bare start", 25 | "test": "jest --forceExit --silent", 26 | "build": "bob build", 27 | "clean": "rm yarn.lock && rm -rf ./node_modules && yarn install", 28 | "lint": "eslint --ext .ts,.tsx .", 29 | "lint:fix": "eslint --ext .ts,.tsx . --fix", 30 | "ts": "tsc --noEmit", 31 | "prepare": "yarn build" 32 | }, 33 | "homepage": "https://github.com/troberts-28/react-native-timer-picker", 34 | "bugs": { 35 | "url": "https://github.com/troberts-28/react-native-timer-picker/issues" 36 | }, 37 | "repository": { 38 | "name": "GitHub", 39 | "type": "git", 40 | "url": "git+https://github.com/troberts-28/react-native-timer-picker.git" 41 | }, 42 | "publishConfig": { 43 | "registry": "https://registry.npmjs.org/" 44 | }, 45 | "files": [ 46 | "dist", 47 | "!**/__tests__", 48 | "!**/__fixtures__", 49 | "!**/__mocks__", 50 | "package.json", 51 | "README.md", 52 | "LICENSE" 53 | ], 54 | "keywords": [ 55 | "react", 56 | "react-native", 57 | "expo", 58 | "duration", 59 | "picker", 60 | "time", 61 | "timer", 62 | "alarm", 63 | "modal", 64 | "durationpicker", 65 | "duration-picker", 66 | "duration-picker-modal", 67 | "react-native-duration-picker", 68 | "react-native-duration-picker-modal", 69 | "timepicker", 70 | "time-picker", 71 | "alarmpicker", 72 | "alarm-picker", 73 | "alarm-picker-modal", 74 | "react-native-alarm-picker", 75 | "timerpicker", 76 | "timer-picker", 77 | "timer-picker-modal", 78 | "react-native-duration-picker-modal", 79 | "expo-duration-picker", 80 | "expo-time-picker", 81 | "expo-timepicker", 82 | "expo-durationpicker", 83 | "expo-duration-picker", 84 | "expo-time-picker", 85 | "expo-timepicker", 86 | "expo-durationpicker" 87 | ], 88 | "engines": { 89 | "node": ">=16.0.0" 90 | }, 91 | "peerDependencies": { 92 | "react": ">=18.2.0", 93 | "react-native": ">=0.72.0" 94 | }, 95 | "devDependencies": { 96 | "@babel/core": "^7.20.0", 97 | "@testing-library/react-native": "^12.0.0", 98 | "@types/jest": "^29.0.0", 99 | "@types/react": ">=18.2.0", 100 | "@types/react-native": "0.72.0", 101 | "@types/react-test-renderer": ">=18.2.0", 102 | "@typescript-eslint/eslint-plugin": "^5.49.0", 103 | "@typescript-eslint/parser": "^5.49.0", 104 | "babel-jest": "^29.6.2", 105 | "eslint": "^8.44.0", 106 | "eslint-plugin-import": "^2.29.0", 107 | "eslint-plugin-react": "^7.33.1", 108 | "eslint-plugin-react-hooks": "^4.6.0", 109 | "eslint-plugin-sort-destructure-keys": "^1.5.0", 110 | "eslint-plugin-typescript-sort-keys": "^2.3.0", 111 | "jest": "^29.0.0", 112 | "metro-react-native-babel-preset": "^0.71.1", 113 | "prettier": "2.8.8", 114 | "react-native-builder-bob": "^0.18.3", 115 | "react-test-renderer": "18.2.0", 116 | "typescript": "^4.7.4" 117 | }, 118 | "react-native-builder-bob": { 119 | "source": "src", 120 | "output": "dist", 121 | "targets": [ 122 | "commonjs", 123 | "module", 124 | "typescript" 125 | ] 126 | }, 127 | "eslintIgnore": [ 128 | "node_modules/", 129 | "dist/" 130 | ] 131 | } 132 | -------------------------------------------------------------------------------- /src/components/DurationScroll/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DurationScroll"; 2 | 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /src/components/DurationScroll/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import type { RefObject } from "react"; 3 | 4 | import type { View } from "react-native"; 5 | 6 | import type { generateStyles } from "../TimerPicker/styles"; 7 | 8 | export interface DurationScrollProps { 9 | Audio?: any; 10 | FlatList?: any; 11 | Haptics?: any; 12 | LinearGradient?: any; 13 | MaskedView?: any; 14 | aggressivelyGetLatestDuration: boolean; 15 | allowFontScaling?: boolean; 16 | amLabel?: string; 17 | clickSoundAsset?: SoundAsset; 18 | decelerationRate?: number | "normal" | "fast"; 19 | disableInfiniteScroll?: boolean; 20 | initialValue?: number; 21 | interval: number; 22 | is12HourPicker?: boolean; 23 | isDisabled?: boolean; 24 | label?: string | React.ReactElement; 25 | limit?: Limit; 26 | maximumValue: number; 27 | onDurationChange: (duration: number) => void; 28 | padNumbersWithZero?: boolean; 29 | padWithNItems: number; 30 | pickerFeedback?: () => void | Promise; 31 | pickerGradientOverlayProps?: Partial; 32 | pmLabel?: string; 33 | repeatNumbersNTimes?: number; 34 | repeatNumbersNTimesNotExplicitlySet: boolean; 35 | styles: ReturnType; 36 | testID?: string; 37 | } 38 | 39 | export interface DurationScrollRef { 40 | latestDuration: RefObject; 41 | reset: (options?: { animated?: boolean }) => void; 42 | setValue: (value: number, options?: { animated?: boolean }) => void; 43 | } 44 | 45 | type LinearGradientPoint = { 46 | x: number; 47 | y: number; 48 | }; 49 | 50 | export type LinearGradientProps = React.ComponentProps & { 51 | colors: string[]; 52 | end?: LinearGradientPoint | null; 53 | locations?: number[] | null; 54 | start?: LinearGradientPoint | null; 55 | }; 56 | 57 | export type Limit = { 58 | max?: number; 59 | min?: number; 60 | }; 61 | 62 | export type SoundAsset = 63 | | number 64 | | { 65 | headers?: Record; 66 | overrideFileExtensionAndroid?: string; 67 | uri: string; 68 | }; 69 | 70 | export type ExpoAvAudioInstance = { 71 | replayAsync: () => Promise; 72 | unloadAsync: () => Promise; 73 | }; 74 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef } from "react"; 2 | 3 | import { 4 | Animated, 5 | Easing, 6 | Modal as ReactNativeModal, 7 | TouchableWithoutFeedback, 8 | useWindowDimensions, 9 | } from "react-native"; 10 | 11 | import { styles } from "./styles"; 12 | import type { ModalProps } from "./types"; 13 | 14 | export const Modal = (props: ModalProps) => { 15 | const { 16 | animationDuration = 300, 17 | children, 18 | contentStyle, 19 | isVisible = false, 20 | modalProps, 21 | onHide, 22 | onOverlayPress, 23 | overlayOpacity = 0.4, 24 | overlayStyle, 25 | testID = "modal", 26 | } = props; 27 | 28 | const { height: screenHeight, width: screenWidth } = useWindowDimensions(); 29 | 30 | const isMounted = useRef(false); 31 | const animatedOpacity = useRef(new Animated.Value(0)); 32 | 33 | useEffect(() => { 34 | isMounted.current = true; 35 | if (isVisible) { 36 | show(); 37 | } 38 | 39 | return () => { 40 | isMounted.current = false; 41 | }; 42 | // eslint-disable-next-line react-hooks/exhaustive-deps 43 | }, []); 44 | 45 | const backdropAnimatedStyle = { 46 | opacity: animatedOpacity.current.interpolate({ 47 | inputRange: [0, 1], 48 | outputRange: [0, overlayOpacity], 49 | }), 50 | }; 51 | const contentAnimatedStyle = { 52 | transform: [ 53 | { 54 | translateY: animatedOpacity.current.interpolate({ 55 | inputRange: [0, 1], 56 | outputRange: [screenHeight, 0], 57 | extrapolate: "clamp", 58 | }), 59 | }, 60 | ], 61 | }; 62 | 63 | const show = useCallback(() => { 64 | Animated.timing(animatedOpacity.current, { 65 | easing: Easing.inOut(Easing.quad), 66 | // Using native driver in the modal makes the content flash 67 | useNativeDriver: true, 68 | duration: animationDuration, 69 | toValue: 1, 70 | }).start(); 71 | }, [animationDuration]); 72 | 73 | const hide = useCallback(() => { 74 | Animated.timing(animatedOpacity.current, { 75 | easing: Easing.inOut(Easing.quad), 76 | // Using native driver in the modal makes the content flash 77 | useNativeDriver: true, 78 | duration: animationDuration, 79 | toValue: 0, 80 | }).start(() => { 81 | if (isMounted.current) { 82 | onHide?.(); 83 | } 84 | }); 85 | }, [animationDuration, onHide]); 86 | 87 | useEffect(() => { 88 | if (isVisible) { 89 | show(); 90 | } else { 91 | hide(); 92 | } 93 | // eslint-disable-next-line react-hooks/exhaustive-deps 94 | }, [isVisible]); 95 | 96 | return ( 97 | 103 | 106 | 114 | 115 | 118 | {children} 119 | 120 | 121 | ); 122 | }; 123 | 124 | export default React.memo(Modal); 125 | -------------------------------------------------------------------------------- /src/components/Modal/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Modal"; 2 | 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /src/components/Modal/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export const styles = StyleSheet.create({ 4 | backdrop: { 5 | position: "absolute", 6 | top: 0, 7 | bottom: 0, 8 | left: 0, 9 | right: 0, 10 | backgroundColor: "black", 11 | opacity: 0, 12 | }, 13 | content: { 14 | flex: 1, 15 | justifyContent: "center", 16 | alignItems: "center", 17 | zIndex: 1, 18 | }, 19 | }); -------------------------------------------------------------------------------- /src/components/Modal/types.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from "react"; 2 | 3 | import type { ViewStyle } from "react-native"; 4 | import type { Modal as ReactNativeModal } from "react-native"; 5 | 6 | export interface ModalProps { 7 | animationDuration?: number; 8 | children?: React.ReactElement; 9 | contentStyle?: ViewStyle; 10 | isVisible?: boolean; 11 | modalProps?: ComponentProps; 12 | onHide?: () => void; 13 | onOverlayPress?: () => void; 14 | overlayOpacity?: number; 15 | overlayStyle?: ViewStyle; 16 | testID?: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/TimerPicker/TimerPicker.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | useEffect, 4 | useImperativeHandle, 5 | useMemo, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | 10 | import { View } from "react-native"; 11 | 12 | import { getSafeInitialValue } from "../../utils/getSafeInitialValue"; 13 | import DurationScroll from "../DurationScroll"; 14 | import type { DurationScrollRef } from "../DurationScroll"; 15 | 16 | import { generateStyles } from "./styles"; 17 | import type { TimerPickerProps, TimerPickerRef } from "./types"; 18 | 19 | const TimerPicker = forwardRef( 20 | (props, ref) => { 21 | const { 22 | aggressivelyGetLatestDuration = false, 23 | allowFontScaling = false, 24 | amLabel = "am", 25 | dayInterval = 1, 26 | dayLabel, 27 | dayLimit, 28 | daysPickerIsDisabled = false, 29 | decelerationRate = 0.88, 30 | disableInfiniteScroll = false, 31 | hideDays = true, 32 | hideHours = false, 33 | hideMinutes = false, 34 | hideSeconds = false, 35 | hourInterval = 1, 36 | hourLabel, 37 | hourLimit, 38 | hoursPickerIsDisabled = false, 39 | initialValue, 40 | maximumDays = 30, 41 | maximumHours = 23, 42 | maximumMinutes = 59, 43 | maximumSeconds = 59, 44 | minuteInterval = 1, 45 | minuteLabel, 46 | minuteLimit, 47 | minutesPickerIsDisabled = false, 48 | onDurationChange, 49 | padDaysWithZero = false, 50 | padHoursWithZero = false, 51 | padMinutesWithZero = true, 52 | padSecondsWithZero = true, 53 | padWithNItems = 1, 54 | pickerContainerProps, 55 | pmLabel = "pm", 56 | repeatDayNumbersNTimes = 3, 57 | repeatHourNumbersNTimes = 8, 58 | repeatMinuteNumbersNTimes = 3, 59 | repeatSecondNumbersNTimes = 3, 60 | secondInterval = 1, 61 | secondLabel, 62 | secondLimit, 63 | secondsPickerIsDisabled = false, 64 | styles: customStyles, 65 | use12HourPicker = false, 66 | ...otherProps 67 | } = props; 68 | 69 | useEffect(() => { 70 | if (otherProps.Audio) { 71 | console.warn( 72 | "The \"Audio\" prop is deprecated and will be removed in a future version. Please use the \"pickerFeedback\" prop instead." 73 | ); 74 | } 75 | if (otherProps.Haptics) { 76 | console.warn( 77 | "The \"Haptics\" prop is deprecated and will be removed in a future version. Please use the \"pickerFeedback\" prop instead." 78 | ); 79 | } 80 | if (otherProps.clickSoundAsset) { 81 | console.warn( 82 | "The \"clickSoundAsset\" prop is deprecated and will be removed in a future version. Please use the \"pickerFeedback\" prop instead." 83 | ); 84 | } 85 | }, [otherProps.Audio, otherProps.Haptics, otherProps.clickSoundAsset]); 86 | 87 | const safePadWithNItems = useMemo(() => { 88 | if (padWithNItems < 0 || isNaN(padWithNItems)) { 89 | return 0; 90 | } 91 | 92 | const maxPadWithNItems = hideHours ? 15 : 6; 93 | 94 | if (padWithNItems > maxPadWithNItems) { 95 | return maxPadWithNItems; 96 | } 97 | 98 | return Math.round(padWithNItems); 99 | }, [hideHours, padWithNItems]); 100 | 101 | const safeInitialValue = useMemo( 102 | () => 103 | getSafeInitialValue({ 104 | days: initialValue?.days, 105 | hours: initialValue?.hours, 106 | minutes: initialValue?.minutes, 107 | seconds: initialValue?.seconds, 108 | }), 109 | [ 110 | initialValue?.days, 111 | initialValue?.hours, 112 | initialValue?.minutes, 113 | initialValue?.seconds, 114 | ] 115 | ); 116 | 117 | const styles = useMemo( 118 | () => generateStyles(customStyles), 119 | 120 | [customStyles] 121 | ); 122 | 123 | const [selectedDays, setSelectedDays] = useState(safeInitialValue.days); 124 | const [selectedHours, setSelectedHours] = useState( 125 | safeInitialValue.hours 126 | ); 127 | const [selectedMinutes, setSelectedMinutes] = useState( 128 | safeInitialValue.minutes 129 | ); 130 | const [selectedSeconds, setSelectedSeconds] = useState( 131 | safeInitialValue.seconds 132 | ); 133 | 134 | useEffect(() => { 135 | onDurationChange?.({ 136 | days: selectedDays, 137 | hours: selectedHours, 138 | minutes: selectedMinutes, 139 | seconds: selectedSeconds, 140 | }); 141 | // eslint-disable-next-line react-hooks/exhaustive-deps 142 | }, [selectedDays, selectedHours, selectedMinutes, selectedSeconds]); 143 | 144 | const daysDurationScrollRef = useRef(null); 145 | const hoursDurationScrollRef = useRef(null); 146 | const minutesDurationScrollRef = useRef(null); 147 | const secondsDurationScrollRef = useRef(null); 148 | 149 | useImperativeHandle(ref, () => ({ 150 | reset: (options) => { 151 | setSelectedDays(safeInitialValue.days); 152 | setSelectedHours(safeInitialValue.hours); 153 | setSelectedMinutes(safeInitialValue.minutes); 154 | setSelectedSeconds(safeInitialValue.seconds); 155 | daysDurationScrollRef.current?.reset(options); 156 | hoursDurationScrollRef.current?.reset(options); 157 | minutesDurationScrollRef.current?.reset(options); 158 | secondsDurationScrollRef.current?.reset(options); 159 | }, 160 | setValue: (value, options) => { 161 | setSelectedDays(value.days); 162 | setSelectedHours(value.hours); 163 | setSelectedMinutes(value.minutes); 164 | setSelectedSeconds(value.seconds); 165 | daysDurationScrollRef.current?.setValue(value.days, options); 166 | hoursDurationScrollRef.current?.setValue(value.hours, options); 167 | minutesDurationScrollRef.current?.setValue( 168 | value.minutes, 169 | options 170 | ); 171 | secondsDurationScrollRef.current?.setValue( 172 | value.seconds, 173 | options 174 | ); 175 | }, 176 | latestDuration: { 177 | days: daysDurationScrollRef.current?.latestDuration, 178 | hours: hoursDurationScrollRef.current?.latestDuration, 179 | minutes: minutesDurationScrollRef.current?.latestDuration, 180 | seconds: secondsDurationScrollRef.current?.latestDuration, 181 | }, 182 | })); 183 | 184 | return ( 185 | 189 | {!hideDays ? ( 190 | 214 | ) : null} 215 | {!hideHours ? ( 216 | 246 | ) : null} 247 | {!hideMinutes ? ( 248 | 273 | ) : null} 274 | {!hideSeconds ? ( 275 | 300 | ) : null} 301 | 302 | ); 303 | } 304 | ); 305 | 306 | export default React.memo(TimerPicker); 307 | -------------------------------------------------------------------------------- /src/components/TimerPicker/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TimerPicker"; 2 | 3 | export * from "./types"; 4 | 5 | export * from "./styles"; 6 | 7 | -------------------------------------------------------------------------------- /src/components/TimerPicker/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import type { TextStyle, ViewStyle } from "react-native"; 3 | 4 | export interface CustomTimerPickerStyles { 5 | backgroundColor?: string; 6 | disabledPickerContainer?: ViewStyle; 7 | disabledPickerItem?: TextStyle; 8 | durationScrollFlatList?: ViewStyle; 9 | durationScrollFlatListContainer?: ViewStyle; 10 | durationScrollFlatListContentContainer?: ViewStyle; 11 | pickerAmPmContainer?: ViewStyle; 12 | pickerAmPmLabel?: TextStyle; 13 | pickerContainer?: ViewStyle & { backgroundColor?: string }; 14 | pickerGradientOverlay?: ViewStyle; 15 | pickerItem?: TextStyle; 16 | pickerItemContainer?: ViewStyle & { height?: number }; 17 | pickerLabel?: TextStyle; 18 | pickerLabelContainer?: ViewStyle; 19 | text?: TextStyle; 20 | theme?: "light" | "dark"; 21 | } 22 | 23 | const DARK_MODE_BACKGROUND_COLOR = "#232323"; 24 | const DARK_MODE_TEXT_COLOR = "#E9E9E9"; 25 | const LIGHT_MODE_BACKGROUND_COLOR = "#F1F1F1"; 26 | const LIGHT_MODE_TEXT_COLOR = "#1B1B1B"; 27 | 28 | export const generateStyles = ( 29 | customStyles: CustomTimerPickerStyles | undefined 30 | ) => 31 | StyleSheet.create({ 32 | pickerContainer: { 33 | flexDirection: "row", 34 | marginRight: "8%", 35 | backgroundColor: 36 | customStyles?.backgroundColor ?? 37 | (customStyles?.theme === "dark" 38 | ? DARK_MODE_BACKGROUND_COLOR 39 | : LIGHT_MODE_BACKGROUND_COLOR), 40 | ...customStyles?.pickerContainer, 41 | }, 42 | pickerLabelContainer: { 43 | position: "absolute", 44 | right: 4, 45 | top: 0, 46 | bottom: 0, 47 | justifyContent: "center", 48 | minWidth: 49 | (customStyles?.pickerLabel?.fontSize ?? 50 | customStyles?.text?.fontSize ?? 51 | 25) * 0.65, 52 | ...customStyles?.pickerLabelContainer, 53 | }, 54 | pickerLabel: { 55 | fontSize: 18, 56 | fontWeight: "bold", 57 | marginTop: 58 | (customStyles?.pickerItem?.fontSize ?? 59 | customStyles?.text?.fontSize ?? 60 | 25) / 6, 61 | color: 62 | customStyles?.theme === "dark" 63 | ? DARK_MODE_TEXT_COLOR 64 | : LIGHT_MODE_TEXT_COLOR, 65 | ...customStyles?.text, 66 | ...customStyles?.pickerLabel, 67 | }, 68 | pickerItemContainer: { 69 | flexDirection: "row", 70 | height: 50, 71 | justifyContent: "center", 72 | alignItems: "center", 73 | width: (customStyles?.pickerItem?.fontSize ?? 25) * 3.6, 74 | ...customStyles?.pickerItemContainer, 75 | }, 76 | pickerItem: { 77 | textAlignVertical: "center", 78 | fontSize: 25, 79 | color: 80 | customStyles?.theme === "dark" 81 | ? DARK_MODE_TEXT_COLOR 82 | : LIGHT_MODE_TEXT_COLOR, 83 | ...customStyles?.text, 84 | ...customStyles?.pickerItem, 85 | }, 86 | pickerAmPmContainer: { 87 | position: "absolute", 88 | right: 0, 89 | top: 0, 90 | bottom: 0, 91 | justifyContent: "center", 92 | ...customStyles?.pickerLabelContainer, 93 | ...customStyles?.pickerAmPmContainer, 94 | }, 95 | pickerAmPmLabel: { 96 | fontSize: 18, 97 | fontWeight: "bold", 98 | marginTop: (customStyles?.pickerItem?.fontSize ?? 25) / 6, 99 | color: 100 | customStyles?.theme === "dark" 101 | ? DARK_MODE_TEXT_COLOR 102 | : LIGHT_MODE_TEXT_COLOR, 103 | ...customStyles?.text, 104 | ...customStyles?.pickerLabel, 105 | ...customStyles?.pickerAmPmLabel, 106 | }, 107 | disabledPickerContainer: { 108 | opacity: 0.4, 109 | ...customStyles?.disabledPickerContainer, 110 | }, 111 | disabledPickerItem: { 112 | opacity: 0.2, 113 | ...customStyles?.disabledPickerItem, 114 | }, 115 | maskedView: { 116 | flex: 1, 117 | }, 118 | pickerGradientOverlay: { 119 | position: "absolute", 120 | width: "100%", 121 | height: "100%", 122 | ...customStyles?.pickerGradientOverlay, 123 | }, 124 | durationScrollFlatList: { 125 | minWidth: 1, 126 | width: "300%", 127 | ...customStyles?.durationScrollFlatList, 128 | }, 129 | durationScrollFlatListContainer: { 130 | overflow: "visible", 131 | ...customStyles?.durationScrollFlatListContainer, 132 | }, 133 | durationScrollFlatListContentContainer: { 134 | ...customStyles?.durationScrollFlatListContentContainer, 135 | }, 136 | }); 137 | -------------------------------------------------------------------------------- /src/components/TimerPicker/types.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject } from "react"; 2 | 3 | import type { View } from "react-native"; 4 | 5 | import type { 6 | LinearGradientProps, 7 | SoundAsset, 8 | Limit, 9 | } from "../DurationScroll/types"; 10 | 11 | import type { CustomTimerPickerStyles } from "./styles"; 12 | 13 | export interface TimerPickerRef { 14 | latestDuration: { 15 | days: RefObject | undefined; 16 | hours: RefObject | undefined; 17 | minutes: RefObject | undefined; 18 | seconds: RefObject | undefined; 19 | }; 20 | reset: (options?: { animated?: boolean }) => void; 21 | setValue: ( 22 | value: { 23 | days: number; 24 | hours: number; 25 | minutes: number; 26 | seconds: number; 27 | }, 28 | options?: { animated?: boolean } 29 | ) => void; 30 | } 31 | 32 | export interface TimerPickerProps { 33 | /** @deprecated Use pickerFeedback prop instead. Will be removed in a future version. */ 34 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 35 | Audio?: any; 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | FlatList?: any; 38 | /** @deprecated Use pickerFeedback prop instead. Will be removed in a future version. */ 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | Haptics?: any; 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | LinearGradient?: any; 43 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 44 | MaskedView?: any; 45 | aggressivelyGetLatestDuration?: boolean; 46 | allowFontScaling?: boolean; 47 | amLabel?: string; 48 | /** @deprecated Use pickerFeedback prop instead. Will be removed in a future version. */ 49 | clickSoundAsset?: SoundAsset; 50 | dayInterval?: number; 51 | dayLabel?: string | React.ReactElement; 52 | dayLimit?: Limit; 53 | daysPickerIsDisabled?: boolean; 54 | decelerationRate?: number | "normal" | "fast"; 55 | disableInfiniteScroll?: boolean; 56 | hideDays?: boolean; 57 | hideHours?: boolean; 58 | hideMinutes?: boolean; 59 | hideSeconds?: boolean; 60 | hourInterval?: number; 61 | hourLabel?: string | React.ReactElement; 62 | hourLimit?: Limit; 63 | hoursPickerIsDisabled?: boolean; 64 | initialValue?: { 65 | days?: number; 66 | hours?: number; 67 | minutes?: number; 68 | seconds?: number; 69 | }; 70 | maximumDays?: number; 71 | maximumHours?: number; 72 | maximumMinutes?: number; 73 | maximumSeconds?: number; 74 | minuteInterval?: number; 75 | minuteLabel?: string | React.ReactElement; 76 | minuteLimit?: Limit; 77 | minutesPickerIsDisabled?: boolean; 78 | onDurationChange?: (duration: { 79 | days: number; 80 | hours: number; 81 | minutes: number; 82 | seconds: number; 83 | }) => void; 84 | padDaysWithZero?: boolean; 85 | padHoursWithZero?: boolean; 86 | padMinutesWithZero?: boolean; 87 | padSecondsWithZero?: boolean; 88 | padWithNItems?: number; 89 | pickerContainerProps?: React.ComponentProps; 90 | pickerFeedback?: () => void | Promise; 91 | pickerGradientOverlayProps?: Partial; 92 | pmLabel?: string; 93 | repeatDayNumbersNTimes?: number; 94 | repeatHourNumbersNTimes?: number; 95 | repeatMinuteNumbersNTimes?: number; 96 | repeatSecondNumbersNTimes?: number; 97 | secondInterval?: number; 98 | secondLabel?: string | React.ReactElement; 99 | secondLimit?: Limit; 100 | secondsPickerIsDisabled?: boolean; 101 | styles?: CustomTimerPickerStyles; 102 | use12HourPicker?: boolean; 103 | } 104 | -------------------------------------------------------------------------------- /src/components/TimerPickerModal/TimerPickerModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | forwardRef, 3 | useCallback, 4 | useEffect, 5 | useImperativeHandle, 6 | useRef, 7 | useState, 8 | } from "react"; 9 | 10 | import { View, Text, TouchableOpacity } from "react-native"; 11 | 12 | import { getSafeInitialValue } from "../../utils/getSafeInitialValue"; 13 | import Modal from "../Modal"; 14 | import TimerPicker from "../TimerPicker"; 15 | import type { TimerPickerRef } from "../TimerPicker"; 16 | 17 | import { generateStyles } from "./styles"; 18 | import type { TimerPickerModalRef, TimerPickerModalProps } from "./types"; 19 | 20 | const TimerPickerModal = forwardRef( 21 | (props, ref) => { 22 | const { 23 | buttonContainerProps, 24 | buttonTouchableOpacityProps, 25 | cancelButtonText = "Cancel", 26 | closeOnOverlayPress, 27 | confirmButtonText = "Confirm", 28 | containerProps, 29 | contentContainerProps, 30 | hideCancelButton = false, 31 | initialValue, 32 | modalProps, 33 | modalTitle, 34 | modalTitleProps, 35 | onCancel, 36 | onConfirm, 37 | onDurationChange, 38 | setIsVisible, 39 | styles: customStyles, 40 | visible, 41 | ...otherProps 42 | } = props; 43 | 44 | const styles = generateStyles(customStyles, { 45 | hasModalTitle: Boolean(modalTitle), 46 | }); 47 | 48 | const timerPickerRef = useRef(null); 49 | 50 | const safeInitialValue = getSafeInitialValue({ 51 | days: initialValue?.days, 52 | hours: initialValue?.hours, 53 | minutes: initialValue?.minutes, 54 | seconds: initialValue?.seconds, 55 | }); 56 | 57 | const [selectedDuration, setSelectedDuration] = 58 | useState(safeInitialValue); 59 | const [confirmedDuration, setConfirmedDuration] = 60 | useState(safeInitialValue); 61 | 62 | const reset = (options?: { animated?: boolean }) => { 63 | setSelectedDuration(safeInitialValue); 64 | setConfirmedDuration(safeInitialValue); 65 | timerPickerRef.current?.reset(options); 66 | }; 67 | 68 | // reset state if the initial value changes 69 | useEffect(() => { 70 | reset(); 71 | // eslint-disable-next-line react-hooks/exhaustive-deps 72 | }, [ 73 | safeInitialValue.days, 74 | safeInitialValue.hours, 75 | safeInitialValue.minutes, 76 | safeInitialValue.seconds, 77 | ]); 78 | 79 | const hideModalHandler = () => { 80 | setSelectedDuration({ 81 | days: confirmedDuration.days, 82 | hours: confirmedDuration.hours, 83 | minutes: confirmedDuration.minutes, 84 | seconds: confirmedDuration.seconds, 85 | }); 86 | setIsVisible(false); 87 | }; 88 | 89 | const confirmHandler = () => { 90 | const latestDuration = timerPickerRef.current?.latestDuration; 91 | 92 | const newDuration = { 93 | days: latestDuration?.days?.current ?? selectedDuration.days, 94 | hours: latestDuration?.hours?.current ?? selectedDuration.hours, 95 | minutes: 96 | latestDuration?.minutes?.current ?? 97 | selectedDuration.minutes, 98 | seconds: 99 | latestDuration?.seconds?.current ?? 100 | selectedDuration.seconds, 101 | }; 102 | setConfirmedDuration(newDuration); 103 | onConfirm(newDuration); 104 | }; 105 | 106 | const cancelHandler = () => { 107 | setIsVisible(false); 108 | setSelectedDuration(confirmedDuration); 109 | onCancel?.(); 110 | }; 111 | 112 | // wrapped in useCallback to avoid unnecessary re-renders of TimerPicker 113 | const durationChangeHandler = useCallback( 114 | (duration: { 115 | days: number; 116 | hours: number; 117 | minutes: number; 118 | seconds: number; 119 | }) => { 120 | setSelectedDuration(duration); 121 | onDurationChange?.(duration); 122 | }, 123 | [onDurationChange] 124 | ); 125 | 126 | useImperativeHandle(ref, () => ({ 127 | reset, 128 | setValue: (value, options) => { 129 | setSelectedDuration(value); 130 | setConfirmedDuration(value); 131 | timerPickerRef.current?.setValue(value, options); 132 | }, 133 | latestDuration: { 134 | days: timerPickerRef.current?.latestDuration?.days, 135 | hours: timerPickerRef.current?.latestDuration?.hours, 136 | minutes: timerPickerRef.current?.latestDuration?.minutes, 137 | seconds: timerPickerRef.current?.latestDuration?.seconds, 138 | }, 139 | })); 140 | 141 | return ( 142 | 149 | 150 | 153 | {modalTitle ? ( 154 | 157 | {modalTitle} 158 | 159 | ) : null} 160 | 168 | 171 | {!hideCancelButton ? ( 172 | 175 | 180 | {cancelButtonText} 181 | 182 | 183 | ) : null} 184 | 187 | 192 | {confirmButtonText} 193 | 194 | 195 | 196 | 197 | 198 | 199 | ); 200 | } 201 | ); 202 | 203 | export default React.memo(TimerPickerModal); 204 | -------------------------------------------------------------------------------- /src/components/TimerPickerModal/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./TimerPickerModal"; 2 | 3 | export * from "./types"; 4 | 5 | export * from "./styles"; 6 | -------------------------------------------------------------------------------- /src/components/TimerPickerModal/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import type { TextStyle, ViewStyle } from "react-native"; 3 | 4 | import type { CustomTimerPickerStyles } from "../TimerPicker/styles"; 5 | 6 | export interface CustomTimerPickerModalStyles extends CustomTimerPickerStyles { 7 | button?: TextStyle; 8 | buttonContainer?: ViewStyle; 9 | cancelButton?: TextStyle; 10 | confirmButton?: TextStyle; 11 | container?: ViewStyle; 12 | contentContainer?: ViewStyle; 13 | modalTitle?: TextStyle; 14 | } 15 | 16 | const DARK_MODE_BACKGROUND_COLOR = "#232323"; 17 | const DARK_MODE_TEXT_COLOR = "#E9E9E9"; 18 | const LIGHT_MODE_BACKGROUND_COLOR = "#F1F1F1"; 19 | const LIGHT_MODE_TEXT_COLOR = "#1B1B1B"; 20 | 21 | export const generateStyles = ( 22 | customStyles: CustomTimerPickerModalStyles | undefined, 23 | variables?: { 24 | hasModalTitle: boolean; 25 | } 26 | ) => { 27 | const { 28 | button: customButtonStyle, 29 | buttonContainer: customButtonContainerStyle, 30 | cancelButton: customCancelButtonStyle, 31 | confirmButton: customConfirmButtonStyle, 32 | container: customContainerStyle, 33 | contentContainer: customContentContainerStyle, 34 | modalTitle: customModalTitleStyle, 35 | ...customTimerPickerStyles 36 | } = customStyles ?? {}; 37 | 38 | return StyleSheet.create({ 39 | container: { 40 | justifyContent: "center", 41 | overflow: "hidden", 42 | ...customContainerStyle, 43 | // disable setting alignItems here because it can affect 44 | // the FlatList's ability to calculate its layout, which can 45 | // stop snapToOffsets working properly 46 | alignItems: undefined, 47 | }, 48 | contentContainer: { 49 | backgroundColor: 50 | customTimerPickerStyles?.backgroundColor ?? 51 | (customTimerPickerStyles?.theme === "dark" 52 | ? DARK_MODE_BACKGROUND_COLOR 53 | : LIGHT_MODE_BACKGROUND_COLOR), 54 | justifyContent: "center", 55 | alignItems: "center", 56 | borderRadius: 20, 57 | overflow: "hidden", 58 | ...customContentContainerStyle, 59 | // disable setting padding here because it can affect 60 | // the FlatList's ability to calculate its layout, which can 61 | // stop snapToOffsets working properly 62 | paddingHorizontal: 0, 63 | paddingVertical: 0, 64 | }, 65 | buttonContainer: { 66 | flexDirection: "row", 67 | marginTop: 25, 68 | marginBottom: 20, 69 | ...customButtonContainerStyle, 70 | }, 71 | button: { 72 | marginHorizontal: 12, 73 | paddingVertical: 10, 74 | paddingHorizontal: 20, 75 | borderWidth: 1, 76 | borderRadius: 10, 77 | fontSize: 16, 78 | overflow: "hidden", 79 | ...customTimerPickerStyles?.text, 80 | ...customButtonStyle, 81 | }, 82 | cancelButton: { 83 | borderColor: "gray", 84 | color: 85 | customTimerPickerStyles?.theme === "dark" 86 | ? DARK_MODE_TEXT_COLOR 87 | : "gray", 88 | backgroundColor: 89 | customTimerPickerStyles?.theme === "dark" ? "gray" : undefined, 90 | ...customTimerPickerStyles?.text, 91 | ...customCancelButtonStyle, 92 | }, 93 | confirmButton: { 94 | borderColor: "green", 95 | color: 96 | customTimerPickerStyles?.theme === "dark" 97 | ? DARK_MODE_TEXT_COLOR 98 | : "green", 99 | backgroundColor: 100 | customTimerPickerStyles?.theme === "dark" ? "green" : undefined, 101 | ...customTimerPickerStyles?.text, 102 | ...customConfirmButtonStyle, 103 | }, 104 | modalTitle: { 105 | fontSize: 24, 106 | fontWeight: "600", 107 | marginTop: 20, 108 | marginBottom: 15, 109 | color: 110 | customTimerPickerStyles?.theme === "dark" 111 | ? DARK_MODE_TEXT_COLOR 112 | : LIGHT_MODE_TEXT_COLOR, 113 | ...customTimerPickerStyles?.text, 114 | ...customModalTitleStyle, 115 | }, 116 | timerPickerStyles: { 117 | ...customTimerPickerStyles, 118 | pickerContainer: { 119 | // set padding here instead of on modal content container because it can affect 120 | // the FlatList's ability to calculate its layout, which can 121 | // stop snapToOffsets working properly 122 | paddingHorizontal: 20, 123 | paddingTop: !variables?.hasModalTitle ? 20 : 0, 124 | ...(customTimerPickerStyles?.pickerContainer ?? {}), 125 | }, 126 | }, 127 | }); 128 | }; 129 | -------------------------------------------------------------------------------- /src/components/TimerPickerModal/types.ts: -------------------------------------------------------------------------------- 1 | import type { RefObject } from "react"; 2 | 3 | import type { View, TouchableOpacity, Text } from "react-native"; 4 | 5 | import type Modal from "../Modal"; 6 | import type { TimerPickerProps } from "../TimerPicker/types"; 7 | 8 | import type { CustomTimerPickerModalStyles } from "./styles"; 9 | 10 | export interface TimerPickerModalRef { 11 | latestDuration: { 12 | days: RefObject | undefined; 13 | hours: RefObject | undefined; 14 | minutes: RefObject | undefined; 15 | seconds: RefObject | undefined; 16 | }; 17 | reset: (options?: { animated?: boolean }) => void; 18 | setValue: ( 19 | value: { 20 | days: number; 21 | hours: number; 22 | minutes: number; 23 | seconds: number; 24 | }, 25 | options?: { animated?: boolean } 26 | ) => void; 27 | } 28 | 29 | export interface TimerPickerModalProps extends TimerPickerProps { 30 | buttonContainerProps?: React.ComponentProps; 31 | buttonTouchableOpacityProps?: React.ComponentProps; 32 | cancelButtonText?: string; 33 | closeOnOverlayPress?: boolean; 34 | confirmButtonText?: string; 35 | containerProps?: React.ComponentProps; 36 | contentContainerProps?: React.ComponentProps; 37 | hideCancelButton?: boolean; 38 | modalProps?: React.ComponentProps; 39 | modalTitle?: string; 40 | modalTitleProps?: React.ComponentProps; 41 | onCancel?: () => void; 42 | onConfirm: ({ 43 | days, 44 | hours, 45 | minutes, 46 | seconds, 47 | }: { 48 | days: number; 49 | hours: number; 50 | minutes: number; 51 | seconds: number; 52 | }) => void; 53 | setIsVisible: (isVisible: boolean) => void; 54 | styles?: CustomTimerPickerModalStyles; 55 | visible: boolean; 56 | } 57 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | default as TimerPickerModal, 3 | TimerPickerModalProps, 4 | TimerPickerModalRef, 5 | CustomTimerPickerModalStyles, 6 | } from "./components/TimerPickerModal"; 7 | 8 | export { 9 | default as TimerPicker, 10 | TimerPickerProps, 11 | TimerPickerRef, 12 | CustomTimerPickerStyles, 13 | } from "./components/TimerPicker"; 14 | -------------------------------------------------------------------------------- /src/tests/DurationScroll.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { render } from "@testing-library/react-native"; 4 | 5 | import DurationScroll from "../components/DurationScroll"; 6 | import type { generateStyles } from "../components/TimerPicker/styles"; 7 | 8 | describe("DurationScroll", () => { 9 | const onDurationChangeMock = jest.fn(); 10 | const emptyStyles = { 11 | pickerContainer: {}, 12 | pickerLabelContainer: {}, 13 | pickerLabel: {}, 14 | pickerItemContainer: {}, 15 | pickerItem: {}, 16 | pickerAmPmContainer: {}, 17 | pickerAmPmLabel: {}, 18 | disabledPickerContainer: {}, 19 | disabledPickerItem: {}, 20 | pickerGradientOverlay: {}, 21 | } as ReturnType; 22 | 23 | it("renders without crashing", () => { 24 | const { getByTestId } = render( 25 | 35 | ); 36 | const component = getByTestId("duration-scroll"); 37 | expect(component).toBeDefined(); 38 | }); 39 | 40 | it("renders the correct number of items", () => { 41 | const { getAllByTestId } = render( 42 | 51 | ); 52 | const items = getAllByTestId("picker-item"); 53 | expect(items).toHaveLength(10); 54 | }); 55 | 56 | it("renders the label if provided", () => { 57 | const { getByText } = render( 58 | 68 | ); 69 | const label = getByText("Duration"); 70 | expect(label).toBeDefined(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/tests/Modal.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { render, fireEvent } from "@testing-library/react-native"; 4 | import { Text } from "react-native"; 5 | 6 | import Modal from "../components/Modal"; 7 | 8 | describe("Modal", () => { 9 | it("renders without crashing", () => { 10 | const { getByTestId } = render(); 11 | const component = getByTestId("modal"); 12 | expect(component).toBeDefined(); 13 | }); 14 | 15 | it("renders children when visible", () => { 16 | const { getByText } = render( 17 | 18 | {"Modal Content"} 19 | 20 | ); 21 | const content = getByText("Modal Content"); 22 | expect(content).toBeDefined(); 23 | }); 24 | 25 | it("calls onOverlayPress when overlay is pressed", () => { 26 | const onOverlayPressMock = jest.fn(); 27 | const { getByTestId } = render( 28 | 29 | ); 30 | const overlay = getByTestId("modal-backdrop"); 31 | fireEvent.press(overlay); 32 | expect(onOverlayPressMock).toHaveBeenCalled(); 33 | }); 34 | 35 | // Add more test cases to cover different interactions, scenarios, and edge cases 36 | }); 37 | -------------------------------------------------------------------------------- /src/tests/TimerPicker.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { render } from "@testing-library/react-native"; 4 | import { FlatList } from "react-native"; 5 | 6 | import TimerPicker from "../components/TimerPicker"; 7 | 8 | describe("TimerPicker", () => { 9 | it("renders without crashing", () => { 10 | const { getByTestId } = render(); 11 | const component = getByTestId("timer-picker"); 12 | expect(component).toBeDefined(); 13 | }); 14 | 15 | it("renders without crashing with negative padWithNItems", () => { 16 | const { getByTestId } = render(); 17 | const component = getByTestId("timer-picker"); 18 | expect(component).toBeDefined(); 19 | }); 20 | 21 | it("hides days, minutes and seconds when respective hide props are provided", () => { 22 | const { queryByTestId } = render( 23 | 24 | ); 25 | const dayPicker = queryByTestId("duration-scroll-day"); 26 | const minutePicker = queryByTestId("duration-scroll-minute"); 27 | const secondPicker = queryByTestId("duration-scroll-second"); 28 | expect(dayPicker).toBeNull(); 29 | expect(minutePicker).toBeNull(); 30 | expect(secondPicker).toBeNull(); 31 | }); 32 | 33 | it("uses the custom FlatList component when provided", () => { 34 | const CustomFlatList = (props) => ( 35 | 36 | ); 37 | const { queryAllByTestId } = render( 38 | 39 | ); 40 | const customFlatList = queryAllByTestId("custom-flat-list"); 41 | expect(customFlatList).toHaveLength(3); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/tests/TimerPickerModal.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { render, fireEvent } from "@testing-library/react-native"; 4 | 5 | import TimerPickerModal from "../components/TimerPickerModal"; 6 | 7 | describe("TimerPickerModal", () => { 8 | const mockOnConfirm = jest.fn(); 9 | const mockOnCancel = jest.fn(); 10 | 11 | const defaultProps = { 12 | visible: true, 13 | setIsVisible: jest.fn(), 14 | onConfirm: mockOnConfirm, 15 | onCancel: mockOnCancel, 16 | }; 17 | 18 | it("renders without crashing", () => { 19 | const { getByTestId } = render(); 20 | const component = getByTestId("timer-picker-modal"); 21 | expect(component).toBeDefined(); 22 | }); 23 | 24 | it("calls onConfirm when Confirm button is pressed", () => { 25 | const { getByText } = render(); 26 | const confirmButton = getByText("Confirm"); 27 | fireEvent.press(confirmButton); 28 | expect(mockOnConfirm).toHaveBeenCalled(); 29 | }); 30 | 31 | it("calls onCancel when Cancel button is pressed", () => { 32 | const { getByText } = render(); 33 | const cancelButton = getByText("Cancel"); 34 | fireEvent.press(cancelButton); 35 | expect(mockOnCancel).toHaveBeenCalled(); 36 | }); 37 | 38 | it("hides the modal when Cancel button is pressed", () => { 39 | const setIsVisibleMock = jest.fn(); 40 | const { getByText } = render( 41 | 45 | ); 46 | const cancelButton = getByText("Cancel"); 47 | fireEvent.press(cancelButton); 48 | expect(setIsVisibleMock).toHaveBeenCalledWith(false); 49 | }); 50 | 51 | it("hides the modal when overlay is pressed", () => { 52 | const setIsVisibleMock = jest.fn(); 53 | const { getByTestId } = render( 54 | 59 | ); 60 | const overlay = getByTestId("modal-backdrop"); 61 | fireEvent.press(overlay); 62 | expect(setIsVisibleMock).toHaveBeenCalledWith(false); 63 | }); 64 | 65 | it("calls onConfirm with selected duration when Confirm button is pressed", () => { 66 | const { getByText } = render(); 67 | // Select duration in TimerPicker, assuming its interaction is tested separately 68 | const confirmButton = getByText("Confirm"); 69 | fireEvent.press(confirmButton); 70 | expect(mockOnConfirm).toHaveBeenCalledWith(expect.objectContaining({})); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/tests/__mocks__/expo-linear-gradient.js: -------------------------------------------------------------------------------- 1 | // __mocks__/expo-linear-gradient.js 2 | import React from "react"; 3 | 4 | export default function LinearGradient(props) { 5 | return
; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/colorToRgba.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts various color formats to RGBA string representation. 3 | * This function is specifically required for expo-linear-gradient on iOS to handle transparent colors correctly. 4 | * It supports named colors, RGB, and hex color formats. 5 | * 6 | * @param {Object} variables - The input variables object 7 | * @param {string} variables.color - The color to convert. Can be: 8 | * - Named color (e.g., 'transparent', 'black', 'white', 'blue', 'green', 'gray', 'red') 9 | * - RGB format (e.g., 'rgb(255, 0, 0)') 10 | * - Hex format (e.g., '#FF0000' or '#F00') 11 | * @param {number} [variables.opacity=1] - The opacity value between 0 and 1 12 | * 13 | * @returns {string} The color in RGBA format (e.g., 'rgba(255, 0, 0, 0.5)') 14 | * 15 | * @example 16 | * // Using named color 17 | * colorToRgba({ color: 'transparent' }) 18 | * // Returns: 'rgba(0, 0, 0, 0)' 19 | * 20 | * @example 21 | * // Using RGB with custom opacity 22 | * colorToRgba({ color: 'rgb(255, 0, 0)', opacity: 0.5 }) 23 | * // Returns: 'rgba(255, 0, 0, 0.5)' 24 | * 25 | * @example 26 | * // Using hex color 27 | * colorToRgba({ color: '#FF0000' }) 28 | * // Returns: 'rgba(255, 0, 0, 1)' 29 | * 30 | * @example 31 | * // Using short hex color 32 | * colorToRgba({ color: '#F00' }) 33 | * // Returns: 'rgba(255, 0, 0, 1)' 34 | */ 35 | export const colorToRgba = (variables: { 36 | color: string; 37 | opacity?: number; 38 | }): string => { 39 | const { color, opacity = 1 } = variables; 40 | 41 | // Handle named colors 42 | const namedColors: { [key: string]: string } = { 43 | transparent: "rgba(0, 0, 0, 0)", 44 | black: "rgba(0, 0, 0, 1)", 45 | white: "rgba(255, 255, 255, 1)", 46 | blue: "rgba(0, 0, 255, 1)", 47 | green: "rgba(0, 128, 0, 1)", 48 | gray: "rgba(128, 128, 128, 1)", 49 | red: "rgba(255, 0, 0, 1)", 50 | }; 51 | 52 | if (color in namedColors) { 53 | return namedColors[color]; 54 | } 55 | 56 | // Handle RGB format 57 | if (color.startsWith("rgb(")) { 58 | const rgbValues = color 59 | .replace("rgb(", "") 60 | .replace(")", "") 61 | .split(",") 62 | .map((value) => parseInt(value.trim(), 10)); 63 | const [r, g, b] = rgbValues; 64 | return `rgba(${r}, ${g}, ${b}, ${opacity})`; 65 | } 66 | 67 | // Handle hex format 68 | if (color.startsWith("#")) { 69 | let hexColor = color.slice(1); 70 | if (hexColor.length === 3) { 71 | hexColor = hexColor 72 | .split("") 73 | .map((value) => value + value) 74 | .join(""); 75 | } 76 | const r = parseInt(hexColor.slice(0, 2), 16); 77 | const g = parseInt(hexColor.slice(2, 4), 16); 78 | const b = parseInt(hexColor.slice(4, 6), 16); 79 | return `rgba(${r}, ${g}, ${b}, ${opacity})`; 80 | } 81 | 82 | return color; // Return unchanged if unable to parse 83 | }; 84 | -------------------------------------------------------------------------------- /src/utils/generateNumbers.ts: -------------------------------------------------------------------------------- 1 | import { padNumber } from "./padNumber"; 2 | 3 | /** 4 | * Generates an array of formatted numbers for a number picker, with support for infinite scroll, 5 | * padding, and number repetition. 6 | * 7 | * @param {number} numberOfItems - Total number of items to generate 8 | * @param {Object} options - Configuration options for number generation 9 | * @param {boolean} [options.disableInfiniteScroll] - Whether to disable infinite scroll 10 | * @param {number} options.interval - The interval between consecutive numbers 11 | * @param {boolean} [options.padNumbersWithZero] - Whether to pad single-digit numbers with leading zeros 12 | * @param {number} options.padWithNItems - Number of empty items to pad with 13 | * @param {number} options.repeatNTimes - How many times to repeat the number sequence 14 | * 15 | * @returns {string[]} Array of formatted number strings 16 | * 17 | * @example 18 | * // Generate numbers 0-9 with padding 19 | * generateNumbers(10, { 20 | * interval: 1, 21 | * padWithNItems: 2, 22 | * repeatNTimes: 1, 23 | * padNumbersWithZero: true 24 | * }) 25 | * // Returns: ['', '', '00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '', ''] 26 | * 27 | * @example 28 | * // Generate even numbers with infinite scroll 29 | * generateNumbers(5, { 30 | * interval: 2, 31 | * padWithNItems: 2, 32 | * repeatNTimes: 3, 33 | * disableInfiniteScroll: false 34 | * }) 35 | * // Returns: ['0', '2', '4', '6', '8', '0', '2', '4', '6', '8', '0', '2', '4', '6', '8'] 36 | */ 37 | export const generateNumbers = ( 38 | numberOfItems: number, 39 | options: { 40 | disableInfiniteScroll?: boolean; 41 | interval: number; 42 | padNumbersWithZero?: boolean; 43 | padWithNItems: number; 44 | repeatNTimes: number; 45 | } 46 | ) => { 47 | if (numberOfItems <= 0) { 48 | return []; 49 | } 50 | 51 | let numbers: string[] = []; 52 | for (let i = 0; i < numberOfItems; i++) { 53 | const value = i * options.interval; 54 | numbers.push( 55 | padNumber(value, { padWithZero: options.padNumbersWithZero }) 56 | ); 57 | } 58 | 59 | if (options.repeatNTimes > 1) { 60 | numbers = Array(options.repeatNTimes).fill(numbers).flat(); 61 | } 62 | if (options.disableInfiniteScroll || options.repeatNTimes === 1) { 63 | numbers.push(...Array(options.padWithNItems).fill("")); 64 | numbers.unshift(...Array(options.padWithNItems).fill("")); 65 | } 66 | return numbers; 67 | }; 68 | 69 | /** 70 | * Generates an array of formatted 12-hour time strings (AM/PM) for a time picker. 71 | * Supports infinite scroll, padding, and number repetition. 72 | * 73 | * @param {Object} options - Configuration options for time generation 74 | * @param {boolean} [options.disableInfiniteScroll] - Whether to disable infinite scroll 75 | * @param {number} options.interval - The interval between hours (must be a divisor of 12) 76 | * @param {boolean} [options.padNumbersWithZero] - Whether to pad single-digit hours with leading zeros 77 | * @param {number} options.padWithNItems - Number of empty items to pad with 78 | * @param {number} [options.repeatNTimes] - How many times to repeat the time sequence (defaults to 1) 79 | * 80 | * @returns {string[]} Array of formatted 12-hour time strings 81 | * 82 | * @example 83 | * // Generate hours with 1-hour interval 84 | * generate12HourNumbers({ 85 | * interval: 1, 86 | * padWithNItems: 2, 87 | * padNumbersWithZero: true 88 | * }) 89 | * // Returns: ['', '', '12 AM', '01 AM', '02 AM', ..., '11 PM', '', ''] 90 | * 91 | * @example 92 | * // Generate hours with 2-hour interval and infinite scroll 93 | * generate12HourNumbers({ 94 | * interval: 2, 95 | * padWithNItems: 2, 96 | * repeatNTimes: 2, 97 | * disableInfiniteScroll: false 98 | * }) 99 | * // Returns: ['12 AM', '2 AM', '4 AM', ..., '10 PM', '12 AM', '2 AM', ...] 100 | */ 101 | export const generate12HourNumbers = (options: { 102 | disableInfiniteScroll?: boolean; 103 | interval: number; 104 | padNumbersWithZero?: boolean; 105 | padWithNItems: number; 106 | repeatNTimes?: number; 107 | }) => { 108 | let numbers: string[] = []; 109 | 110 | // Generate numbers from 0 to 11 for AM 111 | for (let i = 0; i < 12; i += options.interval) { 112 | numbers.push( 113 | `${padNumber(i, { padWithZero: options.padNumbersWithZero })} AM` 114 | ); 115 | } 116 | 117 | // Generate numbers from 12 to 11 for PM 118 | for (let i = 12; i < 24; i += options.interval) { 119 | const hour = i > 12 ? i - 12 : i; 120 | numbers.push( 121 | `${padNumber(hour, { padWithZero: options.padNumbersWithZero })} PM` 122 | ); 123 | } 124 | 125 | if ((options.repeatNTimes ?? 1) > 1) { 126 | numbers = Array(options.repeatNTimes).fill(numbers).flat(); 127 | } 128 | 129 | if (options.disableInfiniteScroll) { 130 | numbers.push(...Array(options.padWithNItems).fill("")); 131 | numbers.unshift(...Array(options.padWithNItems).fill("")); 132 | } 133 | 134 | return numbers; 135 | }; 136 | -------------------------------------------------------------------------------- /src/utils/getAdjustedLimit.ts: -------------------------------------------------------------------------------- 1 | import type { Limit } from "../components/DurationScroll/types"; 2 | 3 | /** 4 | * Adjusts and validates the min/max limits for a scrollable number picker. 5 | * Ensures limits are within valid bounds and handles edge cases. 6 | * 7 | * @param {Limit | undefined} limit - The input limit object containing optional min and max values 8 | * @param {number} numberOfItems - Total number of items in the picker 9 | * @param {number} interval - The interval between consecutive numbers 10 | * 11 | * @returns {{ max: number; min: number }} An object containing the adjusted min and max limits 12 | * 13 | * @example 14 | * // With valid limits 15 | * getAdjustedLimit({ min: 5, max: 15 }, 20, 1) 16 | * // Returns: { max: 15, min: 5 } 17 | * 18 | * @example 19 | * // With out-of-bounds limits 20 | * getAdjustedLimit({ min: -5, max: 25 }, 20, 1) 21 | * // Returns: { max: 19, min: 0 } 22 | * 23 | * @example 24 | * // With invalid limits (max < min) 25 | * getAdjustedLimit({ min: 15, max: 5 }, 20, 1) 26 | * // Returns: { max: 19, min: 0 } 27 | */ 28 | export const getAdjustedLimit = ( 29 | limit: Limit | undefined, 30 | numberOfItems: number, 31 | interval: number 32 | ): { 33 | max: number; 34 | min: number; 35 | } => { 36 | const maxValue = (numberOfItems - 1) * interval; 37 | 38 | if (!limit || (!limit.max && !limit.min)) { 39 | return { 40 | max: maxValue, 41 | min: 0, 42 | }; 43 | } 44 | 45 | // guard against limits that are out of bounds 46 | const adjustedMaxLimit = limit.max 47 | ? Math.min(limit.max, maxValue) 48 | : maxValue; 49 | const adjustedMinLimit = limit.min ? Math.max(limit.min, 0) : 0; 50 | 51 | // guard against invalid limits 52 | if (adjustedMaxLimit < adjustedMinLimit) { 53 | return { 54 | max: maxValue, 55 | min: 0, 56 | }; 57 | } 58 | 59 | return { 60 | max: adjustedMaxLimit, 61 | min: adjustedMinLimit, 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/getDurationAndIndexFromScrollOffset.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculates the duration value and index from a scroll offset in a number picker. 3 | * Handles both infinite and non-infinite scroll modes, taking into account padding and item height. 4 | * 5 | * @param {Object} variables - Configuration object for scroll offset calculation 6 | * @param {boolean} variables.disableInfiniteScroll - Whether infinite scroll is disabled 7 | * @param {number} variables.interval - The interval between consecutive numbers 8 | * @param {number} variables.itemHeight - Height of each item in the picker 9 | * @param {number} variables.numberOfItems - Total number of items in the picker 10 | * @param {number} variables.padWithNItems - Number of empty items to pad with 11 | * @param {number} variables.yContentOffset - The vertical scroll offset 12 | * 13 | * @returns {{ duration: number; index: number }} Object containing the calculated duration and index 14 | * 15 | * @example 16 | * // With infinite scroll enabled 17 | * getDurationAndIndexFromScrollOffset({ 18 | * disableInfiniteScroll: false, 19 | * interval: 1, 20 | * itemHeight: 50, 21 | * numberOfItems: 24, 22 | * padWithNItems: 2, 23 | * yContentOffset: 100 24 | * }) 25 | * // Returns: { duration: 2, index: 2 } 26 | * 27 | * @example 28 | * // With infinite scroll disabled 29 | * getDurationAndIndexFromScrollOffset({ 30 | * disableInfiniteScroll: true, 31 | * interval: 1, 32 | * itemHeight: 50, 33 | * numberOfItems: 24, 34 | * padWithNItems: 2, 35 | * yContentOffset: 100 36 | * }) 37 | * // Returns: { duration: 2, index: 2 } 38 | */ 39 | export const getDurationAndIndexFromScrollOffset = (variables: { 40 | disableInfiniteScroll: boolean; 41 | interval: number; 42 | itemHeight: number; 43 | numberOfItems: number; 44 | padWithNItems: number; 45 | yContentOffset: number; 46 | }) => { 47 | const { 48 | disableInfiniteScroll, 49 | interval, 50 | itemHeight, 51 | numberOfItems, 52 | padWithNItems, 53 | yContentOffset, 54 | } = variables; 55 | 56 | const index = Math.round(yContentOffset / itemHeight); 57 | 58 | const duration = 59 | ((disableInfiniteScroll ? index : index + padWithNItems) % 60 | numberOfItems) * 61 | interval; 62 | 63 | return { 64 | duration, 65 | index, 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/utils/getInitialScrollIndex.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculates the initial scroll index for a number picker based on the desired value and configuration. 3 | * Handles both infinite and non-infinite scroll modes, taking into account padding and repetition. 4 | * 5 | * @param {Object} variables - Configuration object for scroll index calculation 6 | * @param {boolean} variables.disableInfiniteScroll - Whether infinite scroll is disabled 7 | * @param {number} variables.interval - The interval between consecutive numbers 8 | * @param {number} variables.numberOfItems - Total number of items in the picker 9 | * @param {number} variables.padWithNItems - Number of empty items to pad with 10 | * @param {number} variables.repeatNumbersNTimes - How many times to repeat the number sequence 11 | * @param {number} variables.value - The desired initial value 12 | * 13 | * @returns {number} The calculated initial scroll index 14 | * 15 | * @example 16 | * // With infinite scroll enabled 17 | * getInitialScrollIndex({ 18 | * disableInfiniteScroll: false, 19 | * interval: 1, 20 | * numberOfItems: 24, 21 | * padWithNItems: 2, 22 | * repeatNumbersNTimes: 3, 23 | * value: 12 24 | * }) 25 | * // Returns: 38 26 | * 27 | * @example 28 | * // With infinite scroll disabled 29 | * getInitialScrollIndex({ 30 | * disableInfiniteScroll: true, 31 | * interval: 1, 32 | * numberOfItems: 24, 33 | * padWithNItems: 2, 34 | * repeatNumbersNTimes: 1, 35 | * value: 12 36 | * }) 37 | * // Returns: 12 38 | */ 39 | export const getInitialScrollIndex = (variables: { 40 | disableInfiniteScroll: boolean; 41 | interval: number; 42 | numberOfItems: number; 43 | padWithNItems: number; 44 | repeatNumbersNTimes: number; 45 | value: number; 46 | }) => { 47 | const { 48 | disableInfiniteScroll, 49 | interval, 50 | numberOfItems, 51 | padWithNItems, 52 | repeatNumbersNTimes, 53 | value, 54 | } = variables; 55 | 56 | return Math.max( 57 | numberOfItems * Math.floor(repeatNumbersNTimes / 2) + 58 | ((value / interval + numberOfItems) % numberOfItems) - 59 | (!disableInfiniteScroll ? padWithNItems : 0), 60 | 0 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /src/utils/getSafeInitialValue.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Safely extracts and validates initial duration values, ensuring all values are valid numbers. 3 | * Returns a duration object with safe default values (0) for any invalid or missing inputs. 4 | * 5 | * @param {Object} [initialValue] - Optional initial duration values 6 | * @param {number} [initialValue.days] - Initial days value 7 | * @param {number} [initialValue.hours] - Initial hours value 8 | * @param {number} [initialValue.minutes] - Initial minutes value 9 | * @param {number} [initialValue.seconds] - Initial seconds value 10 | * 11 | * @returns {{ days: number; hours: number; minutes: number; seconds: number }} An object containing safe duration values 12 | * 13 | * @example 14 | * // With valid values 15 | * getSafeInitialValue({ days: 1, hours: 2, minutes: 30, seconds: 45 }) 16 | * // Returns: { days: 1, hours: 2, minutes: 30, seconds: 45 } 17 | * 18 | * @example 19 | * // With invalid values 20 | * getSafeInitialValue({ days: NaN, hours: 'invalid', minutes: undefined }) 21 | * // Returns: { days: 0, hours: 0, minutes: 0, seconds: 0 } 22 | * 23 | * @example 24 | * // With undefined input 25 | * getSafeInitialValue(undefined) 26 | * // Returns: { days: 0, hours: 0, minutes: 0, seconds: 0 } 27 | */ 28 | export const getSafeInitialValue = ( 29 | initialValue: 30 | | { 31 | days?: number; 32 | hours?: number; 33 | minutes?: number; 34 | seconds?: number; 35 | } 36 | | undefined 37 | ) => ({ 38 | days: 39 | typeof initialValue?.days === "number" && !isNaN(initialValue?.days) 40 | ? initialValue.days 41 | : 0, 42 | hours: 43 | typeof initialValue?.hours === "number" && !isNaN(initialValue?.hours) 44 | ? initialValue.hours 45 | : 0, 46 | minutes: 47 | typeof initialValue?.minutes === "number" && 48 | !isNaN(initialValue?.minutes) 49 | ? initialValue.minutes 50 | : 0, 51 | seconds: 52 | typeof initialValue?.seconds === "number" && 53 | !isNaN(initialValue?.seconds) 54 | ? initialValue.seconds 55 | : 0, 56 | }); 57 | -------------------------------------------------------------------------------- /src/utils/padNumber.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a number by optionally padding it with a leading zero or space. 3 | * Numbers less than 10 are padded based on the options provided. 4 | * 5 | * @param {number} value - The number to format 6 | * @param {Object} [options] - Optional formatting options 7 | * @param {boolean} [options.padWithZero] - Whether to pad with zero (true) or space (false) 8 | * 9 | * @returns {string} The formatted number string 10 | * 11 | * @example 12 | * // Pad with zero 13 | * padNumber(5, { padWithZero: true }) 14 | * // Returns: '05' 15 | * 16 | * @example 17 | * // Pad with space 18 | * padNumber(5, { padWithZero: false }) 19 | * // Returns: ' 5' 20 | * 21 | * @example 22 | * // No padding needed 23 | * padNumber(15) 24 | * // Returns: '15' 25 | */ 26 | export const padNumber = ( 27 | value: number, 28 | options?: { padWithZero?: boolean } 29 | ): string => { 30 | if (value < 10) { 31 | return (options?.padWithZero ? "0" : " ") + value; 32 | } else { 33 | return String(value); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "lib": ["esnext"], 6 | "allowJs": true, 7 | "jsx": "react-native", 8 | "strict": true, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "rootDir": "src", 14 | "declaration": true, 15 | "paths": { 16 | "react": ["./node_modules/@types/react"] 17 | } 18 | }, 19 | "exclude": [ 20 | "examples/*", 21 | "dist/*", 22 | "src/tests", 23 | "babel.config.js", 24 | "jest.config.js", 25 | "README.md" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------