├── .gitignore ├── CONTRIBUTE.MD ├── README.md ├── package.json ├── preview.gif ├── template.config.js ├── template ├── GITLABRUNNER.MD ├── Gemfile ├── README.md ├── _bundle │ └── config ├── _gitignore ├── _lefthook │ ├── commit-msg │ │ └── verify-commit-msg │ └── pre-commit │ │ └── verify-author ├── _prettierignore ├── _prettierrc.js ├── android │ ├── _gitignore │ ├── app │ │ ├── build.gradle │ │ ├── debug.keystore │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── dev │ │ │ ├── ic_launcher-playstore.png │ │ │ └── res │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_foreground.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ │ └── values │ │ │ │ └── ic_launcher_background.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── helloworld │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainApplication.kt │ │ │ └── res │ │ │ ├── drawable-hdpi │ │ │ └── bootsplash_logo.png │ │ │ ├── drawable-mdpi │ │ │ └── bootsplash_logo.png │ │ │ ├── drawable-xhdpi │ │ │ └── bootsplash_logo.png │ │ │ ├── drawable-xxhdpi │ │ │ └── bootsplash_logo.png │ │ │ ├── drawable-xxxhdpi │ │ │ └── bootsplash_logo.png │ │ │ ├── drawable │ │ │ └── rn_edit_text_material.xml │ │ │ ├── values-night │ │ │ └── colors.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── assets │ ├── appicon │ │ ├── appicon-dev.png │ │ └── appicon.png │ ├── bootsplash │ │ ├── logo.png │ │ ├── logo@1,5x.png │ │ ├── logo@2x.png │ │ ├── logo@3x.png │ │ ├── logo@4x.png │ │ └── manifest.json │ ├── fonts │ │ ├── Manrope-Bold.ttf │ │ ├── Manrope-Medium.ttf │ │ ├── Manrope-SemiBold.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── icons.ttf │ │ └── index.ts │ ├── icon │ │ ├── index.ts │ │ └── source │ │ │ ├── chevron_left.png │ │ │ ├── chevron_right.png │ │ │ └── done.png │ ├── image │ │ ├── index.ts │ │ └── source │ │ │ ├── bg.png │ │ │ ├── bg@2x.png │ │ │ ├── bg@3x.png │ │ │ ├── default.png │ │ │ ├── default@2x.png │ │ │ └── default@3x.png │ └── splash │ │ └── splash.png ├── babel.config.js ├── declare │ ├── @types │ │ ├── array.d.ts │ │ ├── global.d.ts │ │ ├── number.d.ts │ │ └── string.d.ts │ ├── array │ │ └── index.ts │ ├── global │ │ └── index.ts │ ├── index.ts │ ├── number │ │ └── index.ts │ └── string │ │ └── index.ts ├── env-config.ts ├── env │ └── dev.json ├── eslint.config.mjs ├── fastlane │ ├── .env.dev │ ├── FastFile │ ├── HelperFile │ ├── Pluginfile │ ├── README.md │ ├── api-key │ │ ├── apple │ │ │ └── .gitkeep │ │ └── google │ │ │ └── .gitkeep │ ├── metadata │ │ └── android │ │ │ └── en-US │ │ │ └── changelogs │ │ │ └── default.txt │ ├── release-keystore │ │ └── my-upload-key.keystore │ └── report.xml ├── index.ts ├── ios │ ├── .gitignore │ ├── Config.xcconfig │ ├── HelloWorld.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── HelloWorld-Dev.xcscheme │ ├── HelloWorld │ │ ├── AppDelegate.swift │ │ ├── BootSplash.storyboard │ │ ├── Colors.xcassets │ │ │ └── BootSplashBackground-8e2e67.colorset │ │ │ │ └── Contents.json │ │ ├── Helloworld-Bridging-Header.h │ │ ├── Helloworld.entitlements │ │ ├── Images.xcassets │ │ │ ├── AppIcon-Dev.appiconset │ │ │ │ ├── 1024.png │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── 1024.png │ │ │ │ └── Contents.json │ │ │ ├── BootSplashLogo-8e2e67.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── logo-8e2e67.png │ │ │ │ ├── logo-8e2e67@2x.png │ │ │ │ └── logo-8e2e67@3x.png │ │ │ ├── Contents.json │ │ │ └── SplashScreenBackground.colorset │ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── PrivacyInfo.xcprivacy │ │ └── Supporting │ │ │ └── Expo.plist │ ├── Podfile │ ├── Podfile.properties.json │ └── _xcode.env ├── lefthook.yml ├── metro.config.js ├── notification-ios-config.apns ├── package.json ├── patches │ ├── @expo+cli+0.24.14.patch │ └── @expo+config-plugins+10.0.2.patch ├── scripts │ ├── android.js │ ├── build-ci │ │ ├── build-android.js │ │ ├── build-ios.js │ │ ├── deploy-android.js │ │ └── deploy-ios.js │ ├── common.js │ ├── ios.js │ ├── prepare.js │ ├── setup.js │ ├── splash.js │ └── start.js ├── sonar-project.properties ├── src │ ├── app.tsx │ └── app │ │ ├── common │ │ ├── animated │ │ │ ├── hook.ts │ │ │ ├── index.ts │ │ │ ├── math.ts │ │ │ ├── running-animated.ts │ │ │ └── transition.ts │ │ ├── camera-roll │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── constant │ │ │ └── index.ts │ │ ├── emitter │ │ │ ├── event-type.ts │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── firebase │ │ │ ├── index.ts │ │ │ └── notification.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── use-query.ts │ │ ├── method │ │ │ └── index.ts │ │ ├── regex │ │ │ └── index.ts │ │ ├── request-permission │ │ │ └── index.ts │ │ ├── signal │ │ │ └── index.tsx │ │ ├── social-login │ │ │ ├── apple.ts │ │ │ ├── facebook.ts │ │ │ ├── google.ts │ │ │ └── index.ts │ │ ├── socketIo │ │ │ └── index.ts │ │ ├── string │ │ │ └── index.ts │ │ └── zod-validate │ │ │ └── login.ts │ │ ├── library │ │ ├── components │ │ │ ├── button │ │ │ │ ├── default-button.tsx │ │ │ │ ├── hook.ts │ │ │ │ ├── outline-button.tsx │ │ │ │ ├── primary-button.tsx │ │ │ │ ├── styles.ts │ │ │ │ └── type.ts │ │ │ ├── checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── core │ │ │ │ ├── Text.d.ts │ │ │ │ ├── Text.js │ │ │ │ ├── View.d.ts │ │ │ │ ├── View.js │ │ │ │ └── index.ts │ │ │ ├── divider │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── focus-aware-status-bar │ │ │ │ └── index.tsx │ │ │ ├── icon │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── image │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── list-view │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── local-image │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── modal │ │ │ │ ├── index.tsx │ │ │ │ ├── modal-content.tsx │ │ │ │ └── type.ts │ │ │ ├── parsed-text │ │ │ │ ├── index.tsx │ │ │ │ ├── type.ts │ │ │ │ └── utils.ts │ │ │ ├── post-delay │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── radio-button │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── screen │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── snack-bar │ │ │ │ ├── constants.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── snack-bar-item.tsx │ │ │ │ ├── styles.ts │ │ │ │ └── type.ts │ │ │ ├── spacer │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ ├── stack-view │ │ │ │ ├── index.tsx │ │ │ │ └── type.ts │ │ │ └── tabs │ │ │ │ ├── index.tsx │ │ │ │ ├── tab-item.tsx │ │ │ │ └── type.ts │ │ ├── networking │ │ │ ├── api.ts │ │ │ ├── helper.ts │ │ │ └── service.ts │ │ └── utils │ │ │ ├── i18n │ │ │ ├── index.ts │ │ │ └── source │ │ │ │ └── en.json │ │ │ └── storage │ │ │ └── index.ts │ │ ├── model │ │ ├── authentication.ts │ │ └── navigation-params.ts │ │ ├── navigation │ │ ├── app-container.tsx │ │ ├── navigation-service.tsx │ │ ├── root-navigator.tsx │ │ └── screen-types.ts │ │ ├── screens │ │ ├── authentication │ │ │ └── home │ │ │ │ └── index.tsx │ │ └── un-authentication │ │ │ └── login │ │ │ ├── index.tsx │ │ │ └── type.ts │ │ ├── services │ │ ├── app.ts │ │ └── authentication.ts │ │ ├── themes │ │ ├── colors │ │ │ ├── dark.ts │ │ │ └── light.ts │ │ ├── index.ts │ │ ├── text-presets │ │ │ └── index.ts │ │ └── typography │ │ │ └── index.ts │ │ └── zustand │ │ ├── selectors │ │ └── app.ts │ │ └── stores │ │ └── app.ts └── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | lefthook.yml 3 | template/node_modules 4 | template/ios/Pods 5 | template/android/build/ 6 | template/android/app/build/ 7 | node_modules 8 | .idea 9 | .gradle/ 10 | proguard/ 11 | ios/Pods/ 12 | vendor/ 13 | ios/prebuild.log 14 | ios/tmp.xcconfig 15 | .xcode.env.local 16 | yarn.lock 17 | Gemfile.lock 18 | Podfile.lock 19 | local.properties -------------------------------------------------------------------------------- /CONTRIBUTE.MD: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | ### 1: Clone project 4 | 5 | ### 2: Rename "_" file/folder to "." file/folder (Ex:_env -> .env) 6 | > 7 | > Sometime on macos, macos disable copy dot file/folder to another folder, so we use "_" instead of "." to copy file/folder. then we rename it to "." file/folder 8 | > 9 | ```tsx 10 | const UNDERSCORED_DOT_FILES = [ 11 | "babelrc", 12 | "buckconfig", 13 | "eslintrc.js", 14 | "flowconfig", 15 | "gitattributes", 16 | "gitignore", 17 | "prettierignore", 18 | "prettierrc.js", 19 | "editorconfig", 20 | "watchmanconfig", 21 | "bundle", 22 | "lefthook", 23 | "ruby-version", 24 | "xcode.env", 25 | "node-version", 26 | ]; 27 | ``` 28 | 29 | ### 3: Install dependencies 30 | 31 | ### 4: Update code base 32 | 33 | ### 5: Rename "." file/folder to "_" file/folder (Ex: .env ->_env) 34 | 35 | ### 6: Commit and push to your forked repo 36 | 37 | ### 7: Create pull request to this repo 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Welcome to the Boilerplate React Native 2 | 3 | :fireworks: Clean and minimalist React Native template for a quick start with TypeScript and so much more components. 4 | 5 | ## Current version: 0.79.3 6 | 7 | ## :star: Features 8 | 9 | - Elegant usage directly within the RN-Boiler Cli 10 | - Fastlane 11 | - Lefthook 12 | - Boot Splash 13 | - Expo Image 14 | - Declare String, Number, Array 15 | - Consistent with the default React Native template 16 | - Minimal additional dependencies 17 | - Lots of built-in components 18 | - Multiple schema ios(Dev/Prod as default) 19 | - Multiple productFlavors android (dev/prod as default) 20 | 21 | ## Base config (Now u can config on env) 22 | 23 | - Change App name `APP_DISPLAY_NAME` on `env/.dev` 24 | - Change App id `BUNDLE_IDENTIFIER` on `env/.dev` 25 | - Change App version `VERSION_NAME` on `env/.dev` 26 | - Change App build number `VERSION_CODE` on `env/.dev` 27 | - Change App URL `API_URL` on `env/.dev` 28 | 29 | ## :arrow_forward: Usage 30 | 31 | ```sh 32 | npx rn-boiler MyApp 33 | ``` 34 | 35 | Args command: 36 | 37 | ``` 38 | Usage: rn-boiler [options] 39 | 40 | Arguments: 41 | project-name Project name 42 | 43 | Options: 44 | -v, --version Output the current version 45 | -pm, --package-manager Use different package manager (choices: 46 | "yarn", "bun", "npm", default: "bun") 47 | --skip-install Skip install dependencies. Default: false. 48 | (default: false) 49 | --skip-git Skip git init. Default: false. (default: 50 | false) 51 | --verbose Default: false. (default: false) 52 | -h, --help display help for command 53 | ``` 54 | 55 |

Preview

56 | 57 | 58 | ## Library 59 | 60 | - [react-navigation](https://reactnavigation.org) 61 | - [axios](https://axios-http.com) 62 | - [react-hook-form](https://www.react-hook-form.com) 63 | - [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) 64 | - [zod](https://github.com/colinhacks/zod) 65 | - [react-native-bootsplash](https://github.com/zoontek/react-native-bootsplash) 66 | - [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated#readme) 67 | - [zustand](https://zustand.docs.pmnd.rs/getting-started/introduction) 68 | - [expo-image](https://docs.expo.dev/versions/latest/sdk/image/) 69 | - [react-fast-compare](https://github.com/FormidableLabs/react-fast-compare) 70 | 71 | ... and more 72 | 73 | ## :bookmark: License 74 | 75 | This project is [MIT](LICENSE) licensed. 76 | 77 | ## Contribute 78 | 79 | Follow the [contribution guide](CONTRIBUTE.MD) 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rn-boiler-template", 3 | "private": false, 4 | "version": "1.79.0", 5 | "description": "Clean and minimalist React Native template for a quick start with TypeScript and components", 6 | "scripts": { 7 | "test": "exit 0" 8 | }, 9 | "files": [ 10 | "template", 11 | "template.config.js", 12 | "!template/vendor/", 13 | "!template/node_modules/", 14 | "!template/ios/Pods/", 15 | "!template/android/build/", 16 | "!template/android/app/build/", 17 | "!template/android/.idea/", 18 | "!template/android/.gradle/", 19 | "!node_modules/", 20 | "!template/yarn.lock" 21 | ], 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/ngocle2497/BoilerplateReactNative" 25 | }, 26 | "keywords": [ 27 | "react-native", 28 | "typescript", 29 | "template", 30 | "boilerplate", 31 | "starter" 32 | ], 33 | "author": "NgocLe ", 34 | "license": "MIT", 35 | "licenses": [ 36 | { 37 | "type": "MIT" 38 | } 39 | ], 40 | "bugs": { 41 | "url": "https://github.com/ngocle2497/BoilerplateReactNative/issues" 42 | }, 43 | "homepage": "https://github.com/ngocle2497/BoilerplateReactNative#readme" 44 | } 45 | -------------------------------------------------------------------------------- /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/preview.gif -------------------------------------------------------------------------------- /template.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | placeholderName: "HelloWorld", 3 | templateDir: "./template", 4 | }; 5 | -------------------------------------------------------------------------------- /template/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 | gem "fastlane" 13 | 14 | # Ruby 3.4.0 has removed some libraries from the standard library. 15 | gem 'bigdecimal' 16 | gem 'logger' 17 | gem 'benchmark' 18 | gem 'mutex_m' 19 | 20 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 21 | eval(File.read(plugins_path), binding) if File.exist?(plugins_path) -------------------------------------------------------------------------------- /template/_bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /template/_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 | env-config.ts 25 | tmp.xcconfig 26 | # Android/IntelliJ 27 | # 28 | build/ 29 | .idea 30 | .gradle 31 | local.properties 32 | *.iml 33 | *.hprof 34 | .cxx/ 35 | .kotlin/ 36 | *.keystore 37 | !debug.keystore 38 | 39 | # node.js 40 | # 41 | node_modules/ 42 | npm-debug.log 43 | yarn-error.log 44 | 45 | # fastlane 46 | # 47 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 48 | # screenshots whenever they are needed. 49 | # For more information about the recommended setup visit: 50 | # https://docs.fastlane.tools/best-practices/source-control/ 51 | 52 | **/fastlane/report.xml 53 | **/fastlane/Preview.html 54 | **/fastlane/screenshots 55 | **/fastlane/test_output 56 | 57 | # Bundle artifact 58 | *.jsbundle 59 | 60 | # Ruby / CocoaPods 61 | **/Pods/ 62 | /vendor/bundle/ 63 | 64 | # Temporary files created by Metro to check the health of the file watcher 65 | .metro-health-check* 66 | 67 | # testing 68 | /coverage 69 | 70 | # Yarn 71 | .yarn/* 72 | !.yarn/patches 73 | !.yarn/plugins 74 | !.yarn/releases 75 | !.yarn/sdks 76 | !.yarn/versions 77 | 78 | # Expo 79 | .expo 80 | dist/ 81 | web-build/ 82 | 83 | # Sonar 84 | .scannerwork/ -------------------------------------------------------------------------------- /template/_lefthook/commit-msg/verify-commit-msg: -------------------------------------------------------------------------------- 1 | INPUT_FILE=$1 2 | START_LINE=`head -n1 $INPUT_FILE` 3 | 4 | TYPE="feat|fix|refactor|doc|chore|ci|style|test|perf|revert" 5 | PLATFORM="all|android|ios" 6 | 7 | PATTERN="^($TYPE)\(($PLATFORM)\): " 8 | 9 | if ! [[ "$START_LINE" =~ $PATTERN ]]; then 10 | echo "Bad commit message, see example: feat(all): some text" 11 | exit 1 12 | fi 13 | 14 | MESSAGE=$(echo $START_LINE| cut -d':' -f 2 |xargs) 15 | if [ ${#MESSAGE} -lt 12 ]; then 16 | echo "Your message must greater than 12 chacracters" 17 | exit 1 18 | fi -------------------------------------------------------------------------------- /template/_lefthook/pre-commit/verify-author: -------------------------------------------------------------------------------- 1 | AUTHORS=( 2 | 'helloworld:helloworld@gmail.com' 3 | ) 4 | 5 | # AUTHOR_CONFIG="$GIT_AUTHOR_NAME:$GIT_AUTHOR_EMAIL" 6 | 7 | # if [[ ! " ${AUTHORS[*]} " =~ " ${AUTHOR_CONFIG} " ]]; then 8 | # echo ⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫 9 | # echo "Your name should be the same as your username." 10 | # echo "Example: helloworld " 11 | # echo 12 | # echo "Fix your username by using:" 13 | # echo "git config user.name helloworld" 14 | # echo "git config user.email helloworld@gmail.com" 15 | # echo "Then try commiting again" 16 | # echo ⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫⛔️🚫 17 | # exit 1 18 | # fi -------------------------------------------------------------------------------- /template/_prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/build 3 | **/android 4 | **/ios -------------------------------------------------------------------------------- /template/_prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSpacing: true, 4 | jsxBracketSameLine: true, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /template/android/_gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Android/IntelliJ 6 | # 7 | build/ 8 | .idea 9 | .gradle 10 | local.properties 11 | *.iml 12 | *.hprof 13 | .cxx/ 14 | 15 | # Bundle artifacts 16 | *.jsbundle 17 | -------------------------------------------------------------------------------- /template/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/debug.keystore -------------------------------------------------------------------------------- /template/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 | # react-native-reanimated 11 | -keep class com.swmansion.reanimated.** { *; } 12 | -keep class com.facebook.react.turbomodule.** { *; } 13 | 14 | # Add any project specific keep options here: 15 | -------------------------------------------------------------------------------- /template/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /template/android/app/src/dev/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/ic_launcher-playstore.png -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /template/android/app/src/dev/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /template/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 | 25 | 28 | 31 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /template/android/app/src/main/java/com/helloworld/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.helloworld 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | 6 | import com.zoontek.rnbootsplash.RNBootSplash 7 | import com.facebook.react.ReactActivity 8 | import com.facebook.react.ReactActivityDelegate 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 10 | import com.facebook.react.defaults.DefaultReactActivityDelegate 11 | 12 | import expo.modules.ReactActivityDelegateWrapper 13 | 14 | class MainActivity : ReactActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | RNBootSplash.init(this, R.style.BootTheme) 17 | super.onCreate(null) 18 | } 19 | 20 | /** 21 | * Returns the name of the main component registered from JavaScript. This is used to schedule 22 | * rendering of the component. 23 | */ 24 | override fun getMainComponentName(): String = "main" 25 | 26 | /** 27 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 28 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 29 | */ 30 | override fun createReactActivityDelegate(): ReactActivityDelegate { 31 | return ReactActivityDelegateWrapper( 32 | this, 33 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, 34 | object : DefaultReactActivityDelegate( 35 | this, 36 | mainComponentName, 37 | fabricEnabled 38 | ){}) 39 | } 40 | 41 | /** 42 | * Align the back button behavior with Android S 43 | * where moving root activities to background instead of finishing activities. 44 | * @see onBackPressed 45 | */ 46 | override fun invokeDefaultOnBackPressed() { 47 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 48 | if (!moveTaskToBack(false)) { 49 | // For non-root activities, use the default implementation to finish them. 50 | super.invokeDefaultOnBackPressed() 51 | } 52 | return 53 | } 54 | 55 | // Use the default back button implementation on Android S 56 | // because it's doing more than [Activity.moveTaskToBack] in fact. 57 | super.invokeDefaultOnBackPressed() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /template/android/app/src/main/java/com/helloworld/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.helloworld 2 | 3 | import android.app.Application 4 | import android.content.res.Configuration 5 | 6 | import com.facebook.react.PackageList 7 | import com.facebook.react.ReactApplication 8 | import com.facebook.react.ReactNativeHost 9 | import com.facebook.react.ReactPackage 10 | import com.facebook.react.ReactHost 11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 12 | import com.facebook.react.defaults.DefaultReactNativeHost 13 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 14 | import com.facebook.soloader.SoLoader 15 | 16 | import expo.modules.ApplicationLifecycleDispatcher 17 | import expo.modules.ReactNativeHostWrapper 18 | 19 | class MainApplication : Application(), ReactApplication { 20 | 21 | override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( 22 | this, 23 | object : DefaultReactNativeHost(this) { 24 | override fun getPackages(): List { 25 | val packages = PackageList(this).packages 26 | // Packages that cannot be autolinked yet can be added manually here, for example: 27 | // packages.add(MyReactNativePackage()) 28 | return packages 29 | } 30 | 31 | override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" 32 | 33 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 34 | 35 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 36 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 37 | } 38 | ) 39 | 40 | override val reactHost: ReactHost 41 | get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) 42 | 43 | override fun onCreate() { 44 | super.onCreate() 45 | SoLoader.init(this, OpenSourceMergedSoMapping) 46 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 47 | // If you opted-in for the New Architecture, we load the native entry point for this app. 48 | load() 49 | } 50 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 51 | } 52 | 53 | override fun onConfigurationChanged(newConfig: Configuration) { 54 | super.onConfigurationChanged(newConfig) 55 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /template/android/app/src/main/res/drawable-hdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/main/res/drawable-hdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /template/android/app/src/main/res/drawable-mdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/main/res/drawable-mdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /template/android/app/src/main/res/drawable-xhdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/main/res/drawable-xhdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /template/android/app/src/main/res/drawable-xxhdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/main/res/drawable-xxhdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /template/android/app/src/main/res/drawable-xxxhdpi/bootsplash_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/app/src/main/res/drawable-xxxhdpi/bootsplash_logo.png -------------------------------------------------------------------------------- /template/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /template/android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #00FFFFFF 3 | #FFFFFF 4 | #FFFFFF 5 | -------------------------------------------------------------------------------- /template/android/app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /template/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | @string/APP_DISPLAY_NAME 3 | -------------------------------------------------------------------------------- /template/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 22 | 23 | -------------------------------------------------------------------------------- /template/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0' 6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '29') 7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35') 8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '35') 9 | kotlinVersion = findProperty('android.kotlinVersion') ?: '2.0.21' 10 | 11 | ndkVersion = "27.1.12297006" 12 | } 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | dependencies { 18 | classpath('com.android.tools.build:gradle') 19 | classpath('com.facebook.react:react-native-gradle-plugin') 20 | classpath('org.jetbrains.kotlin:kotlin-gradle-plugin') 21 | } 22 | } 23 | 24 | def reactNativeAndroidDir = new File( 25 | providers.exec { 26 | workingDir(rootDir) 27 | commandLine("node", "--print", "require.resolve('react-native/package.json')") 28 | }.standardOutput.asText.get().trim(), 29 | "../android" 30 | ) 31 | 32 | allprojects { 33 | repositories { 34 | maven { 35 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 36 | url(reactNativeAndroidDir) 37 | } 38 | 39 | google() 40 | mavenCentral() 41 | maven { url 'https://www.jitpack.io' } 42 | } 43 | } 44 | 45 | apply plugin: "expo-root-project" 46 | apply plugin: "com.facebook.react.rootproject" 47 | -------------------------------------------------------------------------------- /template/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 | # Enable AAPT2 PNG crunching 26 | android.enablePngCrunchInReleaseBuilds=true 27 | 28 | # Use this property to specify which architecture you want to build. 29 | # You can also override it from the CLI using 30 | # ./gradlew -PreactNativeArchitectures=x86_64 31 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 32 | 33 | # Use this property to enable support to the new architecture. 34 | # This will allow you to use TurboModules and the Fabric render in 35 | # your application. You should enable this flag either if you want 36 | # to write custom TurboModules/Fabric components OR use libraries that 37 | # are providing them. 38 | newArchEnabled=true 39 | 40 | # Use this property to enable or disable the Hermes JS engine. 41 | # If set to false, you will be using JSC instead. 42 | hermesEnabled=true 43 | 44 | # Enable GIF support in React Native images (~200 B increase) 45 | expo.gif.enabled=true 46 | # Enable webp support in React Native images (~85 KB increase) 47 | expo.webp.enabled=true 48 | # Enable animated webp support (~3.4 MB increase) 49 | # Disabled by default because iOS doesn't support animated webp 50 | expo.webp.animated=false 51 | 52 | # Enable network inspector 53 | EX_DEV_CLIENT_NETWORK_INSPECTOR=true 54 | 55 | # Use legacy packaging to compress native libraries in the resulting APK. 56 | expo.useLegacyPackaging=false 57 | 58 | # Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin 59 | expo.edgeToEdgeEnabled=true 60 | android.packagingOptions.pickFirsts=**/libcrypto.so 61 | android.minSdkVersion=29 62 | android.compileSdkVersion=35 63 | android.targetSdkVersion=35 64 | android.buildToolsVersion=35.0.0 -------------------------------------------------------------------------------- /template/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def reactNativeGradlePlugin = new File( 3 | providers.exec { 4 | workingDir(rootDir) 5 | commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") 6 | }.standardOutput.asText.get().trim() 7 | ).getParentFile().absolutePath 8 | includeBuild(reactNativeGradlePlugin) 9 | 10 | def expoPluginsPath = new File( 11 | providers.exec { 12 | workingDir(rootDir) 13 | commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") 14 | }.standardOutput.asText.get().trim(), 15 | "../android/expo-gradle-plugin" 16 | ).absolutePath 17 | includeBuild(expoPluginsPath) 18 | } 19 | 20 | plugins { 21 | id("com.facebook.react.settings") 22 | id("expo-autolinking-settings") 23 | } 24 | 25 | extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> 26 | if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { 27 | ex.autolinkLibrariesFromCommand() 28 | } else { 29 | ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) 30 | } 31 | } 32 | expoAutolinking.useExpoModules() 33 | 34 | rootProject.name = 'HelloWorld' 35 | 36 | expoAutolinking.useExpoVersionCatalog() 37 | 38 | include ':app' 39 | includeBuild(expoAutolinking.reactNativeGradlePlugin) 40 | -------------------------------------------------------------------------------- /template/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "HelloWorld", 4 | "slug": "HelloWorld", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "userInterfaceStyle": "light", 8 | "newArchEnabled": true, 9 | "platforms": ["ios", "android"], 10 | "scheme": "HelloWorld", 11 | "ios": { 12 | "supportsTablet": false, 13 | "bundleIdentifier": "com.HelloWorld" 14 | }, 15 | "android": { 16 | "package": "com.helloworld", 17 | "edgeToEdgeEnabled": true 18 | }, 19 | "plugins": [ 20 | [ 21 | "expo-dev-client", 22 | { 23 | "launchMode": "most-recent" 24 | } 25 | ], 26 | "react-native-keys", 27 | "expo-font" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /template/assets/appicon/appicon-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/appicon/appicon-dev.png -------------------------------------------------------------------------------- /template/assets/appicon/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/appicon/appicon.png -------------------------------------------------------------------------------- /template/assets/bootsplash/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/bootsplash/logo.png -------------------------------------------------------------------------------- /template/assets/bootsplash/logo@1,5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/bootsplash/logo@1,5x.png -------------------------------------------------------------------------------- /template/assets/bootsplash/logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/bootsplash/logo@2x.png -------------------------------------------------------------------------------- /template/assets/bootsplash/logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/bootsplash/logo@3x.png -------------------------------------------------------------------------------- /template/assets/bootsplash/logo@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/bootsplash/logo@4x.png -------------------------------------------------------------------------------- /template/assets/bootsplash/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "#ffffff", 3 | "logo": { 4 | "width": 150, 5 | "height": 150 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /template/assets/fonts/Manrope-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/fonts/Manrope-Bold.ttf -------------------------------------------------------------------------------- /template/assets/fonts/Manrope-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/fonts/Manrope-Medium.ttf -------------------------------------------------------------------------------- /template/assets/fonts/Manrope-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/fonts/Manrope-SemiBold.ttf -------------------------------------------------------------------------------- /template/assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /template/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /template/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /template/assets/fonts/index.ts: -------------------------------------------------------------------------------- 1 | export const fonts = { 2 | icons: require('./icons.ttf'), 3 | manrope_bold: require('./Manrope-Bold.ttf'), 4 | manrope_medium: require('./Manrope-Medium.ttf'), 5 | manrope_semibold: require('./Manrope-SemiBold.ttf'), 6 | roboto_italic: require('./Roboto-Italic.ttf'), 7 | roboto_regular: require('./Roboto-Regular.ttf'), 8 | }; 9 | -------------------------------------------------------------------------------- /template/assets/icon/index.ts: -------------------------------------------------------------------------------- 1 | export const icons = { 2 | done: require('./source/done.png'), 3 | chevron_left: require('./source/chevron_left.png'), 4 | chevron_right: require('./source/chevron_right.png'), 5 | }; 6 | 7 | export type IconTypes = keyof typeof icons; 8 | -------------------------------------------------------------------------------- /template/assets/icon/source/chevron_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/icon/source/chevron_left.png -------------------------------------------------------------------------------- /template/assets/icon/source/chevron_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/icon/source/chevron_right.png -------------------------------------------------------------------------------- /template/assets/icon/source/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/icon/source/done.png -------------------------------------------------------------------------------- /template/assets/image/index.ts: -------------------------------------------------------------------------------- 1 | export const images = { 2 | bg_wallpaper: require('./source/bg.png'), 3 | default: require('./source/default.png'), 4 | }; 5 | 6 | export type ImageTypes = keyof typeof images; 7 | -------------------------------------------------------------------------------- /template/assets/image/source/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/image/source/bg.png -------------------------------------------------------------------------------- /template/assets/image/source/bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/image/source/bg@2x.png -------------------------------------------------------------------------------- /template/assets/image/source/bg@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/image/source/bg@3x.png -------------------------------------------------------------------------------- /template/assets/image/source/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/image/source/default.png -------------------------------------------------------------------------------- /template/assets/image/source/default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/image/source/default@2x.png -------------------------------------------------------------------------------- /template/assets/image/source/default@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/image/source/default@3x.png -------------------------------------------------------------------------------- /template/assets/splash/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/assets/splash/splash.png -------------------------------------------------------------------------------- /template/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | production: { 4 | plugins: ['transform-remove-console'], 5 | }, 6 | }, 7 | plugins: [ 8 | 'react-native-reanimated/plugin', 9 | [ 10 | 'module-resolver', 11 | { 12 | alias: { 13 | '@animated': './src/app/common/animated', 14 | '@app-emitter': './src/app/common/emitter', 15 | '@app-firebase': './src/app/common/firebase', 16 | '@assets': './assets', 17 | '@common': './src/app/common', 18 | '@components': './src/app/library/components', 19 | '@env': './env-config', 20 | '@hooks': './src/app/common/hooks', 21 | '@model': './src/app/model', 22 | '@navigation': './src/app/navigation', 23 | '@networking': './src/app/library/networking', 24 | '@rn-core': './src/app/library/components/core', 25 | '@screens': './src/app/screens', 26 | '@services': './src/app/services', 27 | '@storage': './src/app/library/utils/storage', 28 | '@theme': './src/app/themes', 29 | '@utils': './src/app/library/utils', 30 | '@validate': './src/app/common/zod-validate', 31 | '@zustand': './src/app/zustand', 32 | }, 33 | root: ['./'], 34 | }, 35 | ], 36 | ], 37 | presets: ['babel-preset-expo'], 38 | }; 39 | -------------------------------------------------------------------------------- /template/declare/@types/array.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | declare global { 3 | interface Array { 4 | /** 5 | * Search all elements in array by keyword 6 | */ 7 | searchAllProps(keyword: string | number): Array; 8 | } 9 | 10 | interface ArrayConstructor { 11 | validArray(source: T[]): T[]; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /template/declare/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { AxiosRequestConfig } from 'axios'; 3 | import { z } from 'zod'; 4 | 5 | export {}; 6 | declare module 'react' { 7 | // eslint-disable-next-line @typescript-eslint/ban-types 8 | function forwardRef( 9 | render: ( 10 | props: P, 11 | ref: import('react').ForwardedRef, 12 | ) => import('react').ReactElement | null, 13 | ): ( 14 | props: P & import('react').RefAttributes, 15 | ) => import('react').ReactElement | null; 16 | } 17 | 18 | declare global { 19 | type TypesBase = 20 | | 'bigint' 21 | | 'boolean' 22 | | 'function' 23 | | 'number' 24 | | 'object' 25 | | 'string' 26 | | 'symbol' 27 | | 'undefined'; 28 | 29 | function isTypeof(source: any, type: TypesBase): source is TypesBase; 30 | 31 | function execFunc any>( 32 | func?: Fn, 33 | ...args: Parameters 34 | ): void; 35 | 36 | function randomUniqueId(): string; 37 | 38 | type ActionBase = T extends undefined 39 | ? { 40 | type: string; 41 | } 42 | : { 43 | type: string; 44 | payload: T; 45 | }; 46 | type ZodShape = { 47 | // Require all the keys from T 48 | [key in keyof T]-?: undefined extends T[key] 49 | ? z.ZodOptionalType> 50 | : z.ZodType; 51 | }; 52 | type ReOmit = Pick>; 53 | 54 | type NestedNavigatorParams = { 55 | [K in keyof ParamList]: undefined extends ParamList[K] 56 | ? { screen: K; params?: ParamList[K] } 57 | : { screen: K; params: ParamList[K] }; 58 | }[keyof ParamList]; 59 | 60 | type IncludeMatchingProperties = Pick< 61 | T, 62 | { [K in keyof T]-?: T[K] extends V ? K : never }[keyof T] 63 | >; 64 | 65 | type RequireAtLeastOne = Pick< 66 | T, 67 | Exclude 68 | > & 69 | { 70 | [K in Keys]-?: Required> & Partial>>; 71 | }[Keys]; 72 | 73 | type ResponseBase = { 74 | code: number; 75 | } & (TStatus extends true 76 | ? { 77 | data: T; 78 | 79 | status: true; 80 | } 81 | : { 82 | status: false; 83 | 84 | msg?: string | null; 85 | }); 86 | interface ParamsNetwork extends AxiosRequestConfig { 87 | url: string; 88 | params?: Record; 89 | path?: Record; 90 | body?: Record | FormData; 91 | } 92 | 93 | type ValidateMessageObject = { 94 | keyT: I18nKeys; 95 | optionsTx?: Record; 96 | options?: Record; 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /template/declare/@types/number.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | declare global { 3 | interface Number { 4 | /** 5 | * Convert number to time 6 | */ 7 | toTime(): { hours: number; minutes: number; seconds: number }; 8 | 9 | /** 10 | * Format currency. ex: 1000000 => 1,000,000 11 | * @param comma @default , 12 | */ 13 | currencyFormat(comma?: string): string; 14 | 15 | /** 16 | * Round decimals number. ex: 20.456 => 20.46 17 | * @param maxDecimals @default 2 18 | */ 19 | roundMaxFixed(maxDecimals?: number): number; 20 | 21 | /** 22 | * Convert number to 1K, 1M, 1G, ... if 23 | */ 24 | toStringKMG(digits?: number): string; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /template/declare/@types/string.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | interface String { 5 | /** 6 | * Convert string to camel case 7 | */ 8 | capitalize(): string; 9 | 10 | /** 11 | * Convert all UTF-8 to ASCII lowercase. 12 | */ 13 | changeAlias(): string; 14 | 15 | /** 16 | * Return true if string is empty 17 | */ 18 | isEmpty(): boolean; 19 | 20 | /** 21 | * Remove all characters except 0-9 22 | */ 23 | removeChar(): string; 24 | 25 | /** 26 | * Get all URL from string 27 | */ 28 | getURL(): Array; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /template/declare/array/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable no-extend-native */ 3 | export {}; 4 | 5 | Array.prototype.searchAllProps = function (keyword: string | number) { 6 | return this.filter(x => { 7 | return Object.keys(x).some(function (key) { 8 | return ( 9 | String(x[key]).changeAlias().search(String(keyword).changeAlias()) !== 10 | -1 11 | ); 12 | }); 13 | }); 14 | }; 15 | 16 | Array.validArray = function (source: T[]): T[] { 17 | if (Array.isArray(source)) { 18 | return source; 19 | } 20 | 21 | return [] as T[]; 22 | }; 23 | -------------------------------------------------------------------------------- /template/declare/global/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | globalThis.randomUniqueId = function () { 3 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 4 | // eslint-disable-next-line no-bitwise 5 | const r = (Math.random() * 16) | 0, 6 | // eslint-disable-next-line no-bitwise 7 | v = c === 'x' ? r : (r & 0x3) | 0x8; 8 | 9 | return v.toString(16); 10 | }); 11 | }; 12 | 13 | globalThis.execFunc = function any>( 14 | func?: Fn, 15 | ...args: Parameters 16 | ) { 17 | if (typeof func === 'function') { 18 | func(...args); 19 | } 20 | }; 21 | 22 | globalThis.isTypeof = function ( 23 | source: any, 24 | type: TypesBase, 25 | ): source is TypesBase { 26 | return typeof source === type; 27 | }; 28 | 29 | export {}; 30 | -------------------------------------------------------------------------------- /template/declare/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string'; 2 | 3 | export * from './array'; 4 | 5 | export * from './number'; 6 | 7 | export * from './global'; 8 | -------------------------------------------------------------------------------- /template/declare/number/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-extend-native */ 2 | export {}; 3 | 4 | Number.prototype.toTime = function () { 5 | let totalSeconds = Number(this); 6 | const hours = Math.floor(totalSeconds / 3600); 7 | 8 | totalSeconds %= 3600; 9 | 10 | const minutes = Math.floor(totalSeconds / 60); 11 | 12 | const seconds = totalSeconds % 60; 13 | 14 | return { 15 | hours, 16 | minutes, 17 | seconds, 18 | }; 19 | }; 20 | 21 | Number.prototype.currencyFormat = function (comma = ',') { 22 | const numberStr = this.toString(); 23 | 24 | console.log(numberStr); 25 | 26 | // Split the number into integer and decimal parts 27 | let [integerPart, decimalPart] = numberStr.split('.'); 28 | 29 | // Function to add commas to a part of the number 30 | function addCommas(part: string) { 31 | let result = ''; 32 | let count = 0; 33 | 34 | // Traverse the part from right to left and add commas 35 | for (let i = part.length - 1; i >= 0; i--) { 36 | result = part[i] + result; 37 | 38 | count++; 39 | 40 | if (count === 3 && i !== 0) { 41 | result = comma + result; 42 | 43 | count = 0; 44 | } 45 | } 46 | 47 | return result; 48 | } 49 | 50 | // Format integer and decimal parts 51 | integerPart = addCommas(integerPart); 52 | 53 | if (decimalPart) { 54 | decimalPart = addCommas(decimalPart); 55 | 56 | return `${integerPart}.${decimalPart}`; 57 | } else { 58 | return integerPart; 59 | } 60 | }; 61 | 62 | Number.prototype.roundMaxFixed = function (maxDecimals = 2) { 63 | return Number( 64 | Math.round(Number(String(this + 'e' + maxDecimals))) + 'e-' + maxDecimals, 65 | ); 66 | }; 67 | 68 | Number.prototype.toStringKMG = function () { 69 | const abbreviations = ['', 'k', 'M', 'G', 'T', 'P', 'E']; 70 | 71 | let actualNumber = Number(this); 72 | let index = 0; 73 | while (actualNumber >= 1000 && index < abbreviations.length - 1) { 74 | actualNumber /= 1000; 75 | 76 | index++; 77 | } 78 | 79 | return Math.floor(actualNumber * 10) / 10 + abbreviations[index]; 80 | }; 81 | -------------------------------------------------------------------------------- /template/declare/string/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-extend-native */ 2 | 3 | String.prototype.capitalize = function () { 4 | return this.charAt(0).toUpperCase() + this.slice(1); 5 | }; 6 | 7 | String.prototype.changeAlias = function () { 8 | let str = this.toLowerCase(); 9 | 10 | // Replace Vietnamese diacritics 11 | str = str.replace(/[àáạảãâầấậẩẫăằắặẳẵ]/g, 'a'); 12 | 13 | str = str.replace(/[èéẹẻẽêềếệểễ]/g, 'e'); 14 | 15 | str = str.replace(/[ìíịỉĩ]/g, 'i'); 16 | 17 | str = str.replace(/[òóọỏõôồốộổỗơờớợởỡ]/g, 'o'); 18 | 19 | str = str.replace(/[ùúụủũưừứựửữ]/g, 'u'); 20 | 21 | str = str.replace(/[ỳýỵỷỹ]/g, 'y'); 22 | 23 | str = str.replace(/đ/g, 'd'); 24 | 25 | // Remove special characters 26 | str = str.replace(/[!@%^*()+=<>,.?/:;'"&#[]~$_`{|}|\\-]+/g, ''); 27 | 28 | // Replace multiple spaces with single space 29 | str = str.replace(/ +/g, ' '); 30 | 31 | // Trim leading and trailing spaces 32 | str = str.trim(); 33 | 34 | return str; 35 | }; 36 | 37 | String.prototype.isEmpty = function () { 38 | return this.trim().length === 0; 39 | }; 40 | 41 | String.prototype.removeChar = function () { 42 | return this.replace(/\D/g, ''); 43 | }; 44 | 45 | String.prototype.getURL = function () { 46 | // Simplified URL detection regex 47 | const detectUrls = 48 | /\b(?:https?|ftp):\/\/[-A-Z0-9+&@#/%?=~_|$!:,.;]*[A-Z0-9+&@#/%=~_|$]/gi; 49 | 50 | return this.match(detectUrls) || []; 51 | }; 52 | 53 | export {}; 54 | -------------------------------------------------------------------------------- /template/env-config.ts: -------------------------------------------------------------------------------- 1 | // This file was generated by current env while u run application. 2 | // Do not edit this file as changes may cause incorrect behavior and will be lost 3 | // once the code is regenerated. 4 | 5 | import Keys from 'react-native-keys'; 6 | 7 | export const APP_BUILD_VERSION = '1.0.0.2025.06.07.13.31'; 8 | 9 | export const { BUNDLE_IDENTIFIER } = Keys; 10 | 11 | export const { VERSION_NAME } = Keys; 12 | 13 | export const { VERSION_CODE } = Keys; 14 | 15 | export const { API_URL } = Keys; 16 | 17 | export const { APP_DISPLAY_NAME } = Keys; 18 | 19 | export const { CODE_SIGN_ENTITLEMENTS } = Keys; 20 | 21 | export const { WORKSPACE_NAME } = Keys; 22 | 23 | export const { SCHEME_SUFFIX } = Keys; 24 | 25 | export const { SPLASH_STORYBOARD_NAME } = Keys; 26 | 27 | export const { ASSETCATALOG_COMPILER_APPICON_NAME } = Keys; 28 | 29 | export const { DEBUG_PROVISIONING_PROFILE } = Keys; 30 | 31 | export const { RELEASE_PROVISIONING_PROFILE } = Keys; 32 | 33 | export const { PUBLISH_PROVISIONING_PROFILE } = Keys; 34 | 35 | export const { APPLE_DEVELOPMENT_TEAM } = Keys; 36 | 37 | export const { FLAVOR } = Keys; 38 | 39 | export const PRIVATE_KEY_STORAGE = Keys.secureFor('PRIVATE_KEY_STORAGE'); 40 | -------------------------------------------------------------------------------- /template/env/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "secure": { 3 | "PRIVATE_KEY_STORAGE": "MIICXQIBAAKBgQDQ7TAgBmE3dBFNyukD4HfpzNq3Y5o4J0z4Z3Vxr8iOqCtJSVmaGyrR65dFg/p0ecJh3YF8q6fJb/O9aTwAtAsNlSdv6lcrAnrx4/PuzoK2tg4y852LKuqvRt3EnguExvO2auzdpoDtcKa9usM8C9u19XCxRe3RKKneV6b8opIzpwIDAQAB" 4 | }, 5 | "public": { 6 | "BUNDLE_IDENTIFIER": "com.helloworld", 7 | "VERSION_NAME": "1.0.0", 8 | "VERSION_CODE": "1", 9 | "API_URL": "https:/", 10 | "APP_DISPLAY_NAME": "Hello App Display Name-Dev", 11 | "CODE_SIGN_ENTITLEMENTS": "HelloWorld", 12 | "WORKSPACE_NAME": "HelloWorld", 13 | "SCHEME_SUFFIX": "Dev", 14 | "SPLASH_STORYBOARD_NAME": "BootSplash", 15 | "ASSETCATALOG_COMPILER_APPICON_NAME": "AppIcon-Dev", 16 | "DEBUG_PROVISIONING_PROFILE": "helloworld_dev_development", 17 | "RELEASE_PROVISIONING_PROFILE": "helloworld_dev_ad_hoc", 18 | "PUBLISH_PROVISIONING_PROFILE": "helloworld_dev_appstore", 19 | "APPLE_DEVELOPMENT_TEAM": "", 20 | "FLAVOR": "dev" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /template/fastlane/.env.dev: -------------------------------------------------------------------------------- 1 | EXPORT_APP_ANDROID_NAME_SUFFIX="Android" 2 | EXPORT_APP_IOS_NAME_SUFFIX="IOS" 3 | EXPORT_METHOD="app-store" 4 | GOOGLE_CHAT_WEBHOOK="" 5 | GOOGLE_CHAT_IMAGE_URL="" 6 | GOOGLE_CHAT_THREAD="" 7 | GOOGLE_CHAT_BTN_URL="https://install.appcenter.ms/orgs" 8 | GOOGLE_CHAT_ANDROID_TITLE="HelloWorld Android Staging" 9 | GOOGLE_CHAT_TAG_USER=">" 10 | GOOGLE_CHAT_IOS_TITLE="HelloWorld IOS Staging" 11 | GOOGLE_CHAT_DESCRIPTION="Build for" 12 | APPSTORE_CONNECT_KEY_PATH="" 13 | APPSTORE_CONNECT_KEY_ID="" 14 | APPSTORE_CONNECT_ISSUER_ID="" 15 | GOOGLE_API_KEY_PATH="" 16 | FASTLANE_USER="Your_apple_id" 17 | FASTLANE_PASSWORD="Your apple password" 18 | FASTLANE_ITC_TEAM_NAME="Your_Team_Name" 19 | FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="Your_apple_application_specific_password" 20 | ENVFILE="env/dev.json" 21 | ANDROID_KEY_STORE_FILE="my-upload-key.keystore" 22 | ANDROID_KEY_STORE_PASSWORD="111111" 23 | ANDROID_KEY_STORE_KEY_ALIAS="my-key-alias" 24 | ANDROID_KEY_STORE_KEY_PASSWORD="111111" -------------------------------------------------------------------------------- /template/fastlane/HelperFile: -------------------------------------------------------------------------------- 1 | lane :upload_to_appcenter do |params| 2 | appcenter_upload( 3 | api_token: ENV["APP_CENTER_API_TOKEN"], 4 | owner_name: ENV["APP_CENTER_OWNER_NAME"], 5 | app_name: params[:app_name], 6 | upload_build_only: true, 7 | destinations: ENV['APP_CENTER_DESTINATIONS'], 8 | release_notes: "New release build", 9 | notify_testers: true, 10 | file: params[:file], 11 | ) 12 | end 13 | 14 | lane :notify_testers do |params| 15 | uri = URI.parse(ENV["GOOGLE_CHAT_WEBHOOK"]) 16 | text = params[:text] 17 | thread = { 18 | name: ENV["GOOGLE_CHAT_THREAD"], 19 | } 20 | cards = [ 21 | { 22 | header: { 23 | title: params[:title], 24 | subtitle: ENV["VERSION_NAME"] 25 | }, 26 | sections: [ 27 | { 28 | widgets: [ 29 | { 30 | keyValue: { 31 | topLabel: "Build number", 32 | content: ENV["VERSION_CODE"], 33 | contentMultiline: true 34 | } 35 | } 36 | ] 37 | }, 38 | { 39 | widgets: [ 40 | { 41 | buttons: [ 42 | { 43 | textButton: { 44 | text: "Install app", 45 | onClick: { 46 | openLink: { 47 | url: params[:buttonUrl] 48 | } 49 | } 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | 60 | # Create the HTTP objects 61 | http = Net::HTTP.new(uri.host, uri.port) 62 | http.use_ssl = true 63 | http.verify_mode = OpenSSL::SSL::VERIFY_PEER 64 | request = Net::HTTP::Post.new(uri.request_uri) 65 | request.content_type = "application/json" 66 | request.body = {"text":text, "cards":cards, "thread":thread}.to_json 67 | 68 | # Send the request 69 | response = http.request(request) 70 | case response 71 | when Net::HTTPSuccess 72 | UI.message("Message sent!") 73 | when Net::HTTPServerError 74 | UI.message(response.message) 75 | else 76 | UI.message(response.message) 77 | end 78 | end 79 | 80 | lane :load_env_from_json do |params| 81 | # Load environment variables from the JSON file 82 | env_file = 'env_variables.json' 83 | json_data = JSON.parse(File.read(params[:file])) 84 | 85 | # Set the environment variables 86 | json_data.each do |key, value| 87 | json_data.each do |scope, values| 88 | values.each do |key, value| 89 | ENV["#{key}"] = value 90 | end 91 | end 92 | end 93 | end -------------------------------------------------------------------------------- /template/fastlane/Pluginfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/fastlane/Pluginfile -------------------------------------------------------------------------------- /template/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ### upload_to_appcenter 17 | 18 | ```sh 19 | [bundle exec] fastlane upload_to_appcenter 20 | ``` 21 | 22 | 23 | 24 | ### notify_testers 25 | 26 | ```sh 27 | [bundle exec] fastlane notify_testers 28 | ``` 29 | 30 | 31 | 32 | ### load_env_from_json 33 | 34 | ```sh 35 | [bundle exec] fastlane load_env_from_json 36 | ``` 37 | 38 | 39 | 40 | ---- 41 | 42 | 43 | ## iOS 44 | 45 | ### ios upload_to_TF 46 | 47 | ```sh 48 | [bundle exec] fastlane ios upload_to_TF 49 | ``` 50 | 51 | Upload IPA to TestFlight 52 | 53 | ### ios build_ipa 54 | 55 | ```sh 56 | [bundle exec] fastlane ios build_ipa 57 | ``` 58 | 59 | IOS ipa 60 | 61 | ---- 62 | 63 | 64 | ## Android 65 | 66 | ### android aab_android 67 | 68 | ```sh 69 | [bundle exec] fastlane android aab_android 70 | ``` 71 | 72 | Android build bundle(aab) 73 | 74 | ### android apk_android 75 | 76 | ```sh 77 | [bundle exec] fastlane android apk_android 78 | ``` 79 | 80 | Android build release(apk) 81 | 82 | ### android google_internal 83 | 84 | ```sh 85 | [bundle exec] fastlane android google_internal 86 | ``` 87 | 88 | Android build apk then upload to app center 89 | 90 | ---- 91 | 92 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 93 | 94 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 95 | 96 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 97 | -------------------------------------------------------------------------------- /template/fastlane/api-key/apple/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/fastlane/api-key/apple/.gitkeep -------------------------------------------------------------------------------- /template/fastlane/api-key/google/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/fastlane/api-key/google/.gitkeep -------------------------------------------------------------------------------- /template/fastlane/metadata/android/en-US/changelogs/default.txt: -------------------------------------------------------------------------------- 1 | Fix bugs -------------------------------------------------------------------------------- /template/fastlane/release-keystore/my-upload-key.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/fastlane/release-keystore/my-upload-key.keystore -------------------------------------------------------------------------------- /template/fastlane/report.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /template/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { Text, TextInput } from 'react-native'; 3 | 4 | import { registerRootComponent } from 'expo'; 5 | 6 | // eslint-disable-next-line import/no-extraneous-dependencies 7 | import 'intl-pluralrules'; 8 | import 'react-native-gesture-handler'; 9 | 10 | import 'expo-dev-client'; 11 | import './declare'; 12 | 13 | import { MyApp } from './src/app'; 14 | 15 | // @ts-ignore 16 | Text.defaultProps = Text.defaultProps || { 17 | allowFontScaling: false, 18 | }; 19 | 20 | // @ts-ignore 21 | TextInput.defaultProps = TextInput.defaultProps || { 22 | allowFontScaling: false, 23 | autoCorrect: false, 24 | spellCheck: false, 25 | }; 26 | 27 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 28 | // It also ensures that whether you load the app in Expo Go or in a native build, 29 | // the environment is set up appropriately 30 | registerRootComponent(MyApp); 31 | -------------------------------------------------------------------------------- /template/ios/.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 | project.xcworkspace 24 | .xcode.env.local 25 | 26 | # Bundle artifacts 27 | *.jsbundle 28 | 29 | # CocoaPods 30 | /Pods/ 31 | -------------------------------------------------------------------------------- /template/ios/Config.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "tmp.xcconfig" 2 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Expo 2 | import React 3 | import ReactAppDependencyProvider 4 | 5 | @UIApplicationMain 6 | public class AppDelegate: ExpoAppDelegate { 7 | var window: UIWindow? 8 | 9 | var reactNativeDelegate: ExpoReactNativeFactoryDelegate? 10 | var reactNativeFactory: RCTReactNativeFactory? 11 | 12 | public override func application( 13 | _ application: UIApplication, 14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 15 | ) -> Bool { 16 | let delegate = ReactNativeDelegate() 17 | let factory = ExpoReactNativeFactory(delegate: delegate) 18 | delegate.dependencyProvider = RCTAppDependencyProvider() 19 | 20 | reactNativeDelegate = delegate 21 | reactNativeFactory = factory 22 | bindReactNativeFactory(factory) 23 | 24 | #if os(iOS) || os(tvOS) 25 | window = UIWindow(frame: UIScreen.main.bounds) 26 | factory.startReactNative( 27 | withModuleName: "main", 28 | in: window, 29 | launchOptions: launchOptions) 30 | #endif 31 | 32 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 33 | } 34 | 35 | // Linking API 36 | public override func application( 37 | _ app: UIApplication, 38 | open url: URL, 39 | options: [UIApplication.OpenURLOptionsKey: Any] = [:] 40 | ) -> Bool { 41 | return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) 42 | } 43 | 44 | // Universal Links 45 | public override func application( 46 | _ application: UIApplication, 47 | continue userActivity: NSUserActivity, 48 | restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void 49 | ) -> Bool { 50 | let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) 51 | return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result 52 | } 53 | } 54 | 55 | class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { 56 | // Extension point for config-plugins 57 | 58 | override func sourceURL(for bridge: RCTBridge) -> URL? { 59 | // needed to return the correct URL for expo-dev-client. 60 | bridge.bundleURL ?? bundleURL() 61 | } 62 | 63 | override func customize(_ rootView: UIView) { 64 | super.customize(rootView) 65 | RNBootSplash.initWithStoryboard(Keys.public(for: "SPLASH_STORYBOARD_NAME"), rootView: rootView) 66 | } 67 | 68 | override func bundleURL() -> URL? { 69 | #if DEBUG 70 | return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") 71 | #else 72 | return Bundle.main.url(forResource: "main", withExtension: "jsbundle") 73 | #endif 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Colors.xcassets/BootSplashBackground-8e2e67.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "idiom": "universal", 5 | "color": { 6 | "color-space": "srgb", 7 | "components": { 8 | "blue": "1.00000000000000", 9 | "green": "1.00000000000000", 10 | "red": "1.00000000000000", 11 | "alpha": "1.000" 12 | } 13 | } 14 | } 15 | ], 16 | "info": { 17 | "author": "xcode", 18 | "version": 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Helloworld-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | #import "Keys.h" 5 | #import "RNBootSplash.h" 6 | #import 7 | #import 8 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Helloworld.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/AppIcon-Dev.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/ios/HelloWorld/Images.xcassets/AppIcon-Dev.appiconset/1024.png -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/AppIcon-Dev.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"filename":"1024.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}} 2 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/ios/HelloWorld/Images.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"filename":"1024.png","idiom":"universal","platform":"ios","size":"1024x1024"}],"info":{"author":"xcode","version":1}} 2 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "logo-8e2e67.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "filename": "logo-8e2e67@2x.png", 11 | "scale": "2x" 12 | }, 13 | { 14 | "idiom": "universal", 15 | "filename": "logo-8e2e67@3x.png", 16 | "scale": "3x" 17 | } 18 | ], 19 | "info": { 20 | "author": "xcode", 21 | "version": 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/logo-8e2e67.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/logo-8e2e67.png -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/logo-8e2e67@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/logo-8e2e67@2x.png -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/logo-8e2e67@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/ios/HelloWorld/Images.xcassets/BootSplashLogo-8e2e67.imageset/logo-8e2e67@3x.png -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Images.xcassets/SplashScreenBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": [ 3 | { 4 | "color": { 5 | "components": { 6 | "alpha": "1.000", 7 | "blue": "1.00000000000000", 8 | "green": "1.00000000000000", 9 | "red": "1.00000000000000" 10 | }, 11 | "color-space": "srgb" 12 | }, 13 | "idiom": "universal" 14 | } 15 | ], 16 | "info": { 17 | "version": 1, 18 | "author": "expo" 19 | } 20 | } -------------------------------------------------------------------------------- /template/ios/HelloWorld/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | $(APP_DISPLAY_NAME) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(APP_DISPLAY_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | $(VERSION_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | HelloWorld 31 | com.HelloWorld 32 | 33 | 34 | 35 | CFBundleURLSchemes 36 | 37 | exp+helloworld 38 | 39 | 40 | 41 | CFBundleVersion 42 | $(VERSION_CODE) 43 | LSMinimumSystemVersion 44 | 12.0 45 | LSRequiresIPhoneOS 46 | 47 | NSAppTransportSecurity 48 | 49 | NSAllowsArbitraryLoads 50 | 51 | NSAllowsLocalNetworking 52 | 53 | 54 | UILaunchStoryboardName 55 | $(SPLASH_STORYBOARD_NAME) 56 | UIRequiredDeviceCapabilities 57 | 58 | arm64 59 | 60 | UIRequiresFullScreen 61 | 62 | UIStatusBarStyle 63 | UIStatusBarStyleLightContent 64 | UISupportedInterfaceOrientations 65 | 66 | UIInterfaceOrientationPortrait 67 | 68 | UISupportedInterfaceOrientations~ipad 69 | 70 | UIInterfaceOrientationPortrait 71 | UIInterfaceOrientationPortraitUpsideDown 72 | 73 | UIUserInterfaceStyle 74 | Automatic 75 | UIViewControllerBasedStatusBarAppearance 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryUserDefaults 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | CA92.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryFileTimestamp 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | 0A2A.1 21 | 3B52.1 22 | C617.1 23 | 24 | 25 | 26 | NSPrivacyAccessedAPIType 27 | NSPrivacyAccessedAPICategoryDiskSpace 28 | NSPrivacyAccessedAPITypeReasons 29 | 30 | E174.1 31 | 85F4.1 32 | 33 | 34 | 35 | NSPrivacyAccessedAPIType 36 | NSPrivacyAccessedAPICategorySystemBootTime 37 | NSPrivacyAccessedAPITypeReasons 38 | 39 | 35F9.1 40 | 41 | 42 | 43 | NSPrivacyCollectedDataTypes 44 | 45 | NSPrivacyTracking 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /template/ios/HelloWorld/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | 12 | -------------------------------------------------------------------------------- /template/ios/Podfile: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") 2 | require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") 3 | 4 | require 'json' 5 | podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} 6 | 7 | ENV['RCT_NEW_ARCH_ENABLED'] = '0' if podfile_properties['newArchEnabled'] == 'false' 8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] 9 | ENV['RCT_IGNORE_PODS_DEPRECATION'] = '1' 10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1' 11 | install! 'cocoapods', 12 | :deterministic_uuids => false 13 | 14 | prepare_react_native_project! 15 | 16 | target 'HelloWorld' do 17 | use_expo_modules! 18 | 19 | if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' 20 | config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; 21 | else 22 | config_command = [ 23 | 'npx', 24 | 'expo-modules-autolinking', 25 | 'react-native-config', 26 | '--json', 27 | '--platform', 28 | 'ios' 29 | ] 30 | end 31 | 32 | config = use_native_modules!(config_command) 33 | 34 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 35 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 36 | 37 | use_react_native!( 38 | :path => config[:reactNativePath], 39 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 40 | # An absolute path to your application root. 41 | :app_path => "#{Pod::Config.instance.installation_root}/..", 42 | :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', 43 | ) 44 | 45 | post_install do |installer| 46 | react_native_post_install( 47 | installer, 48 | config[:reactNativePath], 49 | :mac_catalyst_enabled => false, 50 | :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', 51 | ) 52 | 53 | # This is necessary for Xcode 14, because it signs resource bundles by default 54 | # when building for devices. 55 | installer.target_installation_results.pod_target_installation_results 56 | .each do |pod_name, target_installation_result| 57 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 58 | resource_bundle_target.build_configurations.each do |config| 59 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /template/ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", 4 | "newArchEnabled": "true", 5 | "ios.deploymentTarget": "15.1", 6 | "apple.privacyManifestAggregationEnabled": "true" 7 | } 8 | -------------------------------------------------------------------------------- /template/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 | -------------------------------------------------------------------------------- /template/lefthook.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngocle2497/BoilerplateReactNative/754127733a263d9e7ff061ef13cdbac6b7533b33/template/lefthook.yml -------------------------------------------------------------------------------- /template/metro.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { mergeConfig } = require('@react-native/metro-config'); 4 | 5 | const { getDefaultConfig } = require('expo/metro-config'); 6 | 7 | /** 8 | * Metro configuration 9 | * https://reactnative.dev/docs/metro 10 | * 11 | * @type {import('metro-config').MetroConfig} 12 | */ 13 | const config = {}; 14 | 15 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 16 | -------------------------------------------------------------------------------- /template/notification-ios-config.apns: -------------------------------------------------------------------------------- 1 | { 2 | "aps":{ 3 | "alert":"Test noti", 4 | "sound":"default", 5 | "badge":1 6 | }, 7 | "key_data":"key_data", 8 | "other_key_data":"key_data", 9 | "gcm.message_id":"20021" 10 | } -------------------------------------------------------------------------------- /template/patches/@expo+config-plugins+10.0.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@expo/config-plugins/build/android/Package.js b/node_modules/@expo/config-plugins/build/android/Package.js 2 | index cbc7688..22cc5e3 100644 3 | --- a/node_modules/@expo/config-plugins/build/android/Package.js 4 | +++ b/node_modules/@expo/config-plugins/build/android/Package.js 5 | @@ -281,7 +281,7 @@ async function getApplicationIdAsync(projectRoot) { 6 | return null; 7 | } 8 | const buildGradle = await _fs().default.promises.readFile(buildGradlePath, 'utf8'); 9 | - const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/); 10 | + const matchResult = buildGradle.match(/applicationId ['"](.*)['"]/) ?? buildGradle.match(/namespace ['"](.*)['"]/); 11 | // TODO add fallback for legacy cases to read from AndroidManifest.xml 12 | return matchResult?.[1] ?? null; 13 | } 14 | -------------------------------------------------------------------------------- /template/scripts/build-ci/build-android.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync } = require('child_process'); 3 | 4 | (() => { 5 | const [envArgs, export_dir] = process.argv.slice(2); 6 | 7 | console.log(`envArgs: ${envArgs}`); 8 | 9 | envArgs.split(',').forEach(envArg => { 10 | console.log(`Building android for ${envArg}`); 11 | 12 | execSync( 13 | `bundle exec fastlane android aab_android --env ${envArg} export_dir:${export_dir}`, 14 | { 15 | stdio: 'inherit', 16 | }, 17 | ); 18 | 19 | execSync( 20 | `bundle exec fastlane android apk_android --env ${envArg} export_dir:${export_dir}`, 21 | { 22 | stdio: 'inherit', 23 | }, 24 | ); 25 | }); 26 | })(); 27 | -------------------------------------------------------------------------------- /template/scripts/build-ci/build-ios.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync } = require('child_process'); 3 | 4 | (() => { 5 | const [envArgs, export_dir] = process.argv.slice(2); 6 | 7 | console.log(`envArgs: ${envArgs}`); 8 | 9 | envArgs.split(',').forEach(envArg => { 10 | console.log(`Building ios for ${envArg}`); 11 | 12 | execSync( 13 | `bundle exec fastlane ios build_ipa --env ${envArg} export_dir:${export_dir}`, 14 | { 15 | stdio: 'inherit', 16 | }, 17 | ); 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /template/scripts/build-ci/deploy-android.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync } = require('child_process'); 3 | 4 | (() => { 5 | const [envArgs, export_dir] = process.argv.slice(2); 6 | 7 | console.log(`envArgs: ${envArgs}`); 8 | 9 | envArgs.split(',').forEach(envArg => { 10 | console.log(`Deploying android for ${envArg}`); 11 | 12 | execSync( 13 | `bundle exec fastlane android google_internal --env ${envArg} export_dir:${export_dir}`, 14 | { 15 | stdio: 'inherit', 16 | }, 17 | ); 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /template/scripts/build-ci/deploy-ios.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync } = require('child_process'); 3 | 4 | (() => { 5 | const [envArgs, export_dir] = process.argv.slice(2); 6 | 7 | console.log(`envArgs: ${envArgs}`); 8 | 9 | envArgs.split(',').forEach(envArg => { 10 | console.log(`Deploying ios for ${envArg}`); 11 | 12 | execSync( 13 | `bundle exec fastlane ios upload_to_TF --env ${envArg} export_dir:${export_dir}`, 14 | { 15 | stdio: 'inherit', 16 | }, 17 | ); 18 | }); 19 | })(); 20 | -------------------------------------------------------------------------------- /template/scripts/common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { execSync, spawnSync } = require('child_process'); 4 | 5 | const { readFileSync, writeFileSync } = require('fs'); 6 | 7 | const { join } = require('path'); 8 | 9 | const padStart = number => { 10 | if (number.toString().length < 2) { 11 | return `0${number}`; 12 | } 13 | 14 | return String(number); 15 | }; 16 | 17 | const getEnvJsonFromPath = envPath => { 18 | const data = readFileSync( 19 | join(__dirname.replace('scripts', ''), envPath), 20 | 'utf8', 21 | ); 22 | 23 | if (data) { 24 | return JSON.parse(data); 25 | } 26 | 27 | throw new Error('ENV file not found'); 28 | }; 29 | 30 | const setupEnv = envPath => { 31 | let infoJsEnv = `// This file was generated by current env while u run application. 32 | // Do not edit this file as changes may cause incorrect behavior and will be lost 33 | // once the code is regenerated. 34 | 35 | import Keys from 'react-native-keys'; 36 | 37 | `; 38 | 39 | const envJson = getEnvJsonFromPath(envPath); 40 | 41 | console.log({ envJson }); 42 | 43 | const todayDate = new Date(); 44 | 45 | const year = todayDate.getFullYear(); 46 | 47 | const month = todayDate.getMonth() + 1; 48 | 49 | const date = todayDate.getDate(); 50 | 51 | const hours = todayDate.getHours(); 52 | 53 | const minutes = todayDate.getMinutes(); 54 | 55 | const APP_BUILD_VERSION = `${year}.${padStart(month)}.${padStart( 56 | date, 57 | )}.${padStart(hours)}.${padStart(minutes)}`; 58 | 59 | infoJsEnv += `export const APP_BUILD_VERSION = '${envJson.public.VERSION_NAME.replace( 60 | '"', 61 | '', 62 | ).replace('"', '')}.${APP_BUILD_VERSION}';`; 63 | 64 | // loop to add variable to env-config.ts 65 | Object.keys(envJson?.public ?? {}).forEach(key => { 66 | infoJsEnv += `\n\nexport const { ${key} } = Keys;`; 67 | }); 68 | 69 | Object.keys(envJson?.secure ?? {}).forEach(key => { 70 | infoJsEnv += `\n\nexport const ${key} = Keys.secureFor('${key}');`; 71 | }); 72 | 73 | infoJsEnv += '\n'; 74 | 75 | // remove cache 76 | spawnSync('rm -rf $TMPDIR/metro-*'); 77 | 78 | spawnSync('rm -rf node_modules/.cache/babel-loader/*'); 79 | 80 | // write env-config.ts 81 | writeFileSync( 82 | join(__dirname.replace('scripts', ''), 'env-config.ts'), 83 | infoJsEnv, 84 | 'utf8', 85 | ); 86 | 87 | console.log('✨✨✨✨✨ SET UP Env done ✨✨✨✨✨'); 88 | 89 | return envJson; 90 | }; 91 | 92 | const getAndroidHome = () => { 93 | try { 94 | return ( 95 | execSync('echo $ANDROID_HOME').toString().trim() || 96 | execSync('echo $ANDROID_SDK_ROOT').toString().trim() 97 | ); 98 | } catch { 99 | return ''; 100 | } 101 | }; 102 | 103 | module.exports = { 104 | getAndroidHome, 105 | getEnvJsonFromPath, 106 | setupEnv, 107 | }; 108 | -------------------------------------------------------------------------------- /template/scripts/ios.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync } = require('child_process'); 3 | 4 | const { getEnvJsonFromPath } = require('./common'); 5 | 6 | const bootDevice = deviceName => { 7 | /** 8 | * iPhone 8 (2292DF56-3D4D-4328-90CF-C804E8A1A7F5) (Shutdown) 9 | * iPhone 11 (7590DD1E-D039-4545-ADB1-586F2471D873) (Booted) 10 | * iPhone 11 Pro (24C004AB-A617-4168-B0F2-37FD9223A1C0) (Booted) 11 | */ 12 | try { 13 | // if simulator is not booted, it will throw an error 14 | execSync( 15 | `xcrun simctl list devices | grep "${deviceName}" | grep "Booted"`, 16 | ); 17 | } catch { 18 | execSync(`xcrun simctl boot "${deviceName}"`); 19 | } 20 | 21 | return execSync( 22 | `xcrun simctl list devices | grep "${deviceName}" | grep "Booted" | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})"`, 23 | ).toString(); 24 | }; 25 | 26 | const uninstallOldApp = bundleId => { 27 | // uninstall app using xcrun 28 | execSync(`xcrun simctl uninstall booted "${bundleId}"`); 29 | }; 30 | 31 | const run = ({ envPath }) => { 32 | const envJson = getEnvJsonFromPath(envPath); 33 | 34 | const simulator = 'iPhone 16'; 35 | 36 | const udid = bootDevice(simulator); 37 | 38 | uninstallOldApp(envJson.public.BUNDLE_IDENTIFIER); 39 | 40 | execSync( 41 | `npx expo run:ios --app-id ${envJson.public.BUNDLE_IDENTIFIER} --scheme ${envJson.public.WORKSPACE_NAME}-${envJson.public.SCHEME_SUFFIX} --device ${udid}`, 42 | { stdio: 'inherit' }, 43 | ); 44 | }; 45 | 46 | const pushNotification = ({ envPath }) => { 47 | const envJson = getEnvJsonFromPath(envPath); 48 | 49 | const simulator = 'iPhone 14 Pro'; 50 | 51 | const deviceId = bootDevice(simulator); 52 | 53 | execSync( 54 | `xcrun simctl push ${deviceId} ${envJson.public.BUNDLE_IDENTIFIER} notification-ios-config.apns`, 55 | { stdio: 'inherit' }, 56 | ); 57 | }; 58 | 59 | (() => { 60 | const { argv, platform } = process; 61 | 62 | if (platform !== 'darwin') { 63 | console.log('This script is only for macOS'); 64 | 65 | return; 66 | } 67 | 68 | const actualArgv = argv.slice(2); 69 | 70 | const [nameFunc, envPath] = actualArgv; 71 | 72 | switch (nameFunc) { 73 | case 'run': 74 | run({ envPath }); 75 | 76 | break; 77 | case 'push-notification': 78 | pushNotification({ envPath }); 79 | 80 | break; 81 | 82 | default: 83 | break; 84 | } 85 | })(); 86 | -------------------------------------------------------------------------------- /template/scripts/prepare.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const { setupEnv } = require('./common'); 4 | 5 | (() => { 6 | const { argv } = process; 7 | 8 | const [, , envPath] = argv; 9 | 10 | setupEnv(envPath); 11 | })(); 12 | -------------------------------------------------------------------------------- /template/scripts/setup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync, spawnSync } = require('child_process'); 3 | 4 | const { getAndroidHome } = require('./common'); 5 | 6 | (function () { 7 | try { 8 | execSync('npx --yes patch-package', { stdio: 'inherit' }); 9 | 10 | spawnSync('npx lefthook install', { stdio: 'inherit' }); 11 | 12 | if (getAndroidHome() !== '') { 13 | execSync( 14 | `echo "sdk.dir=${getAndroidHome()}" > android/local.properties`, 15 | { 16 | stdio: 'inherit', 17 | }, 18 | ); 19 | } 20 | 21 | if (process.platform === 'darwin') { 22 | execSync('cd ios && touch tmp.xcconfig'); 23 | 24 | console.log( 25 | ' 🧐🧐🧐🧐🧐 Installing Bundle dependencies!! 🧐🧐🧐🧐🧐', 26 | ); 27 | 28 | execSync('bundle install', { 29 | stdio: 'inherit', 30 | }); 31 | 32 | console.log('bundle install Done!!✨✨✨✨✨'); 33 | 34 | console.log( 35 | ' 🧐🧐🧐🧐🧐 Installing CocoaPods dependencies!! 🧐🧐🧐🧐🧐', 36 | ); 37 | 38 | execSync( 39 | 'bundle exec pod install --project-directory=ios --repo-update', 40 | { 41 | stdio: 'inherit', 42 | }, 43 | ); 44 | 45 | console.log(' ✨✨✨✨✨ Pod done!!! ✨✨✨✨✨'); 46 | } 47 | } catch {} 48 | })(); 49 | -------------------------------------------------------------------------------- /template/scripts/splash.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/order */ 2 | /* eslint-disable padding-line-between-statements */ 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | const { execSync } = require('child_process'); 5 | 6 | (function () { 7 | const { argv } = process; 8 | 9 | const actualArgv = argv.slice(2); 10 | 11 | const [path, bgColor, width, flavor] = actualArgv; 12 | execSync( 13 | `yarn react-native-bootsplash generate ${path} --background=${bgColor} --platforms=android,ios --project-type=bare --logo-width=${width} --assets-output=assets/bootsplash --flavor=${flavor}`, 14 | { stdio: 'inherit' }, 15 | ); 16 | })(); 17 | -------------------------------------------------------------------------------- /template/scripts/start.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { execSync } = require('child_process'); 3 | 4 | const { getEnvJsonFromPath } = require('./common'); 5 | 6 | (() => { 7 | const { argv } = process; 8 | 9 | const actualArgv = argv.slice(2); 10 | 11 | const [envPath] = actualArgv; 12 | 13 | const envJson = getEnvJsonFromPath(envPath); 14 | 15 | execSync( 16 | `npx expo start --app-id ${envJson.public.BUNDLE_IDENTIFIER} --clear`, 17 | { 18 | stdio: 'inherit', 19 | }, 20 | ); 21 | })(); 22 | -------------------------------------------------------------------------------- /template/sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Helloworld 2 | sonar.projectName=Helloworld 3 | sonar.projectVersion=1.0 4 | sonar.sources=./src,./declare 5 | sonar.sourceEncoding=UTF-8 6 | sonar.host.url=http://localhost:9000 7 | sonar.exclusions=**/themes/colors/**,**/.yarn/** 8 | -------------------------------------------------------------------------------- /template/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, Suspense, useState } from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { I18nextProvider } from 'react-i18next'; 5 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 6 | import { KeyboardProvider as RNKeyboardProvider } from 'react-native-keyboard-controller'; 7 | import { SafeAreaProvider } from 'react-native-safe-area-context'; 8 | import { UnistylesProvider } from 'react-native-unistyles'; 9 | 10 | import { PortalProvider } from '@gorhom/portal'; 11 | import { useDidMount } from '@hooks'; 12 | import { AppContainer } from '@navigation/app-container'; 13 | import { useLoadFont } from '@theme/typography'; 14 | import I18n from '@utils/i18n'; 15 | 16 | import './app/themes/index'; 17 | 18 | const styles = StyleSheet.create({ 19 | root: { 20 | flex: 1, 21 | }, 22 | }); 23 | 24 | const KeyboardProvider = ({ children }: { children?: ReactNode }) => { 25 | // state 26 | const [loading, setLoading] = useState(true); 27 | 28 | // effect 29 | useDidMount(() => { 30 | queueMicrotask(() => { 31 | setLoading(false); 32 | }); 33 | }); 34 | 35 | // render 36 | return ( 37 | <>{loading ? null : {children}} 38 | ); 39 | }; 40 | 41 | export const MyApp = () => { 42 | // state 43 | const isLoaded = useLoadFont(); 44 | 45 | if (!isLoaded) { 46 | return null; 47 | } 48 | 49 | // render 50 | return ( 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /template/src/app/common/animated/hook.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtrapolationType, 3 | interpolate, 4 | interpolateColor, 5 | SharedValue, 6 | useDerivedValue, 7 | } from 'react-native-reanimated'; 8 | 9 | import { sharedClamp, sharedMax, sharedMin } from './math'; 10 | 11 | /** 12 | * Interpolate number 13 | */ 14 | export const useInterpolate = ( 15 | progress: SharedValue, 16 | input: number[], 17 | output: number[], 18 | type?: ExtrapolationType, 19 | // eslint-disable-next-line max-params 20 | ) => useDerivedValue(() => interpolate(progress.value, input, output, type)); 21 | 22 | /** 23 | * Interpolate color 24 | */ 25 | export const useInterpolateColor = ( 26 | progress: SharedValue, 27 | input: number[], 28 | output: string[], 29 | colorSpace?: 'RGB' | 'HSV' | undefined, 30 | // eslint-disable-next-line max-params 31 | ) => { 32 | 'worklet'; 33 | 34 | return useDerivedValue(() => 35 | interpolateColor(progress.value, input, output, colorSpace), 36 | ); 37 | }; 38 | 39 | /** 40 | * Linear interpolation between x and y using a to weight between them 41 | */ 42 | export const useMix = (progress: SharedValue, x: number, y: number) => { 43 | 'worklet'; 44 | 45 | return useDerivedValue(() => x + progress.value * (y - x)); 46 | }; 47 | 48 | /** 49 | * Convert number to radian 50 | */ 51 | export const useRadian = (value: SharedValue) => 52 | useDerivedValue(() => { 53 | 'worklet'; 54 | 55 | return `${value.value}deg`; 56 | }); 57 | 58 | /** 59 | * Clamp value when out of range 60 | */ 61 | export const useShareClamp = ( 62 | value: SharedValue, 63 | lowerValue: number, 64 | upperValue: number, 65 | ) => { 66 | 'worklet'; 67 | 68 | return useDerivedValue(() => 69 | sharedClamp(value.value, lowerValue, upperValue), 70 | ); 71 | }; 72 | 73 | /** 74 | * Return min number of args 75 | */ 76 | export const useMin = (...args: SharedValue[]) => { 77 | 'worklet'; 78 | 79 | return useDerivedValue(() => sharedMin(...args.map(x => x.value))); 80 | }; 81 | 82 | /** 83 | * Return max number of args 84 | */ 85 | export const useMax = (...args: SharedValue[]) => { 86 | 'worklet'; 87 | 88 | return useDerivedValue(() => sharedMax(...args.map(x => x.value))); 89 | }; 90 | -------------------------------------------------------------------------------- /template/src/app/common/animated/index.ts: -------------------------------------------------------------------------------- 1 | export * from './math'; 2 | 3 | export * from './transition'; 4 | 5 | export * from './hook'; 6 | 7 | export * from './running-animated'; 8 | -------------------------------------------------------------------------------- /template/src/app/common/animated/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Clamp value on UI thread. 3 | */ 4 | export const sharedClamp = ( 5 | value: number, 6 | lowerValue: number, 7 | upperValue: number, 8 | ) => { 9 | 'worklet'; 10 | 11 | return Math.min(Math.max(lowerValue, value), upperValue); 12 | }; 13 | 14 | /** 15 | * Takes two or more animated nodes or values, and when evaluated, 16 | * returns the result of subtracting their values in the exact order on UI thread 17 | */ 18 | export const sharedSub = (...args: number[]) => { 19 | 'worklet'; 20 | if (args.length <= 0) { 21 | return 0; 22 | } 23 | 24 | return args 25 | .slice(1) 26 | .reduce((accumulator, curr) => accumulator - curr, args[0]); 27 | }; 28 | 29 | /** 30 | * Get min number of array parameters on UI thread. 31 | */ 32 | export const sharedMin = (...args: number[]) => { 33 | 'worklet'; 34 | 35 | return Math.min.call(null, ...args); 36 | }; 37 | 38 | /** 39 | * Get max number of array parameters on UI thread. 40 | */ 41 | export const sharedMax = (...args: number[]) => { 42 | 'worklet'; 43 | 44 | return Math.max.call(null, ...args); 45 | }; 46 | 47 | /** 48 | * Select a point where the animation should snap to given the value of the gesture and it's velocity on UI thread. 49 | */ 50 | export const sharedSnapPoint = ( 51 | value: number, 52 | velocity: number, 53 | points: number[], 54 | ) => { 55 | 'worklet'; 56 | const point = value + velocity * 0.2; 57 | 58 | const diffPoint = (p: number) => Math.abs(point - p); 59 | 60 | const deltas = points.map(p => diffPoint(p)); 61 | 62 | const minDelta = sharedMin(...deltas); 63 | 64 | return points.reduce((acc, p) => (diffPoint(p) === minDelta ? p : acc), 0); 65 | }; 66 | 67 | /** 68 | * Convert radian to degree on UI thread. 69 | */ 70 | export const sharedToDeg = (rad: number) => { 71 | 'worklet'; 72 | 73 | return (rad * 180) / Math.PI; 74 | }; 75 | 76 | /** 77 | * Convert degree to radian on UI thread. 78 | */ 79 | export const sharedToRad = (deg: number) => { 80 | 'worklet'; 81 | 82 | return (deg * Math.PI) / 180; 83 | }; 84 | 85 | /** 86 | * Calculator the average value of an array parameters UI thread. 87 | */ 88 | export const sharedAvg = (...args: number[]) => { 89 | 'worklet'; 90 | 91 | return args.reduce((a, v) => a + v, 0) / args.length; 92 | }; 93 | 94 | /** 95 | * Round number of UI thread. 96 | */ 97 | export const sharedRound = (value: number, precision = 0) => { 98 | 'worklet'; 99 | const p = Math.pow(10, precision); 100 | 101 | return Math.round(value * p) / p; 102 | }; 103 | 104 | /** 105 | * Convert boolean to 0 or 1 on UI thead 106 | */ 107 | export const sharedBin = (value: boolean): 0 | 1 => { 108 | 'worklet'; 109 | 110 | return value ? 1 : 0; 111 | }; 112 | -------------------------------------------------------------------------------- /template/src/app/common/animated/transition.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { 4 | Easing, 5 | SharedValue, 6 | useDerivedValue, 7 | useSharedValue, 8 | withSpring, 9 | WithSpringConfig, 10 | withTiming, 11 | WithTimingConfig, 12 | } from 'react-native-reanimated'; 13 | 14 | import { sharedBin } from './math'; 15 | 16 | /** 17 | * Return value runs from 0 to 1 when state change using withTiming 18 | */ 19 | export const useSharedTransition = ( 20 | state: boolean | number, 21 | config?: WithTimingConfig, 22 | initialValue?: number, 23 | ): SharedValue => { 24 | const value = useSharedValue(initialValue ?? 0); 25 | 26 | useEffect(() => { 27 | value.value = typeof state === 'boolean' ? sharedBin(state) : state; 28 | }, [state, value]); 29 | 30 | return useDerivedValue(() => 31 | withTiming(value.value, { 32 | duration: 500, 33 | easing: Easing.bezier(0.33, 0.01, 0, 1), 34 | ...(config ?? {}), 35 | }), 36 | ); 37 | }; 38 | 39 | /** 40 | * Return value runs from 0 to 1 when state change using withSpring 41 | */ 42 | export const useSharedSpringTransition = ( 43 | state: boolean, 44 | config?: WithSpringConfig, 45 | initialValue?: number, 46 | ): SharedValue => { 47 | const value = useSharedValue(initialValue ?? 0); 48 | 49 | useEffect(() => { 50 | value.value = typeof state === 'boolean' ? sharedBin(state) : state; 51 | }, [state, value]); 52 | 53 | return useDerivedValue(() => withSpring(value.value, config)); 54 | }; 55 | -------------------------------------------------------------------------------- /template/src/app/common/camera-roll/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks'; 2 | -------------------------------------------------------------------------------- /template/src/app/common/camera-roll/type.ts: -------------------------------------------------------------------------------- 1 | type AssetType = 'All' | 'Videos' | 'Photos'; 2 | 3 | export interface GalleryOptions { 4 | pageSize?: number; 5 | assetType: AssetType; 6 | } 7 | 8 | export type MediaType = 'image' | 'video'; 9 | 10 | export interface Media { 11 | uri: string; 12 | type: MediaType; 13 | playableDuration?: number; 14 | } 15 | 16 | export interface GalleryLogic { 17 | medias: Array; 18 | loadNextPage: () => void; 19 | isLoading: boolean; 20 | isLoadingNextPage: boolean; 21 | isReloading: boolean; 22 | hasNextPage: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /template/src/app/common/constant/index.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from 'react-native'; 2 | 3 | export const MMKV_KEY = { 4 | APP_TOKEN: 'APP_TOKEN', 5 | } as const; 6 | 7 | export const API_CONFIG = { 8 | CODE_DEFAULT: -200, 9 | CODE_SUCCESS: 200, 10 | CODE_TIME_OUT: 408, 11 | ERROR_NETWORK_CODE: -100, 12 | RESULT_CODE_PUSH_OUT: 401, 13 | STATUS_TIME_OUT: 'ECONNABORTED', 14 | TIME_OUT: 10 * 1000, 15 | }; 16 | 17 | export const SLICE_NAME = { 18 | APP: 'APP_', 19 | AUTHENTICATION: 'AUTHENTICATION_', 20 | }; 21 | 22 | export const isIos = Platform.OS === 'ios'; 23 | -------------------------------------------------------------------------------- /template/src/app/common/emitter/event-type.ts: -------------------------------------------------------------------------------- 1 | export type EventSelectDate = { 2 | from: string | null; 3 | to: string | null; 4 | }; 5 | -------------------------------------------------------------------------------- /template/src/app/common/emitter/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { EventKeyName, EventParamsList, Listeners } from './type'; 3 | 4 | export { EVENT_NAME } from './type'; 5 | 6 | export type { EventKeyName } from './type'; 7 | 8 | export * from './event-type'; 9 | 10 | const listeners: Listeners = []; 11 | 12 | export const subscribeEvent = ( 13 | ...args: T extends EventKeyName 14 | ? [ 15 | eventKey: T, 16 | listener: undefined extends EventParamsList[T] 17 | ? () => void 18 | : (data: EventParamsList[T]) => void, 19 | ] 20 | : [eventKey: T, listener: (data: P) => void] 21 | ) => { 22 | const uuid = randomUniqueId(); 23 | 24 | listeners.push({ 25 | eventKey: args[0], 26 | listener: args[1], 27 | uuid, 28 | }); 29 | 30 | return () => { 31 | const index = listeners.findIndex(x => x.uuid === uuid); 32 | 33 | listeners.splice(index, 1); 34 | }; 35 | }; 36 | 37 | export const emitEvent = ( 38 | ...args: T extends EventKeyName 39 | ? undefined extends EventParamsList[T] 40 | ? [eventName: T] 41 | : [eventName: T, payload: P | EventParamsList[T]] 42 | : [eventName: T, payload?: P] 43 | ) => { 44 | for (const element of listeners) { 45 | if (element.eventKey === args[0]) { 46 | element.listener(args[1]); 47 | } 48 | } 49 | }; 50 | 51 | export const unSubscribeAllEvent = () => { 52 | listeners.length = 0; 53 | }; 54 | -------------------------------------------------------------------------------- /template/src/app/common/emitter/type.ts: -------------------------------------------------------------------------------- 1 | import { EventSelectDate } from './event-type'; 2 | 3 | export const EVENT_NAME = { 4 | DATE_SELECTED: 'DATE_SELECTED', 5 | } as const; 6 | 7 | export interface EventParamsList { 8 | [EVENT_NAME.DATE_SELECTED]: EventSelectDate; 9 | } 10 | 11 | export type EventKeyName = keyof EventParamsList; 12 | 13 | export type ListenerCallback = T extends EventKeyName 14 | ? (data: EventParamsList[T]) => void 15 | : (data: P) => void; 16 | 17 | export type Listeners = Array<{ 18 | uuid: string; 19 | eventKey: string; 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | listener: any; 22 | }>; 23 | -------------------------------------------------------------------------------- /template/src/app/common/firebase/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | /** 3 | import * as notification from './notification'; 4 | 5 | export const appFirebase = { 6 | notification, 7 | }; 8 | */ 9 | -------------------------------------------------------------------------------- /template/src/app/common/firebase/notification.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * remove this line when use 3 | */ 4 | export {}; 5 | /* eslint-disable @typescript-eslint/no-explicit-any */ 6 | /** 7 | import { useEffect } from 'react'; 8 | 9 | import { requestNotifications } from 'react-native-permissions'; 10 | 11 | import messaging, { 12 | FirebaseMessagingTypes, 13 | } from '@react-native-firebase/messaging'; 14 | 15 | export interface RemoteNotification 16 | extends ReOmit { 17 | // Nested data from fcm is string. carefully when use 18 | // example data:{ nested:{ a: 1 }} 19 | // => nested will be string 20 | data?: T; 21 | } 22 | 23 | export const requestNotificationPermission = async () => { 24 | return new Promise(resolve => { 25 | requestNotifications(['alert', 'sound', 'badge']) 26 | .then(res => { 27 | resolve(res.status === 'granted'); 28 | }) 29 | .catch(() => { 30 | resolve(false); 31 | }); 32 | }); 33 | }; 34 | 35 | export const getDeviceToken = async () => { 36 | return new Promise(resolve => { 37 | messaging() 38 | .getToken() 39 | .then(resolve) 40 | .catch(() => { 41 | resolve(''); 42 | }); 43 | }); 44 | }; 45 | 46 | export const useInAppNotification = ( 47 | callback: (remoteNotification: RemoteNotification) => any, 48 | ) => { 49 | // effect 50 | useEffect(() => { 51 | const unsubscribeInApp = messaging().onMessage( 52 | callback as (message: FirebaseMessagingTypes.RemoteMessage) => any, 53 | ); 54 | 55 | return () => { 56 | unsubscribeInApp(); 57 | }; 58 | }, []); 59 | }; 60 | 61 | export const useNotificationOpened = ( 62 | callback: (remoteNotification: RemoteNotification) => any, 63 | ) => { 64 | // effect 65 | useEffect(() => { 66 | const unsubscribeBackground = messaging().onNotificationOpenedApp( 67 | callback as (message: FirebaseMessagingTypes.RemoteMessage) => any, 68 | ); 69 | 70 | messaging().setBackgroundMessageHandler( 71 | callback as (message: FirebaseMessagingTypes.RemoteMessage) => any, 72 | ); 73 | 74 | messaging() 75 | .getInitialNotification() 76 | .then(res => { 77 | if (res) { 78 | callback(res as any); 79 | } 80 | }); 81 | 82 | return () => { 83 | unsubscribeBackground(); 84 | }; 85 | // eslint-disable-next-line react-hooks/exhaustive-deps 86 | }, []); 87 | }; 88 | */ 89 | -------------------------------------------------------------------------------- /template/src/app/common/method/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { Alert, ColorValue, Linking } from 'react-native'; 4 | 5 | import { processColor } from 'react-native-reanimated'; 6 | 7 | export const onShowErrorBase = (msg: string) => { 8 | Alert.alert(msg); 9 | }; 10 | 11 | export const openLinking = (url: string) => { 12 | Linking.canOpenURL(url).then(supported => { 13 | if (supported) { 14 | Linking.openURL(url); 15 | } 16 | }); 17 | }; 18 | 19 | export const setAlpha = (color: ColorValue, alpha = 1) => { 20 | 'worklet'; 21 | let num = typeof color === 'number' ? color : processColor(color); 22 | 23 | if (typeof num !== 'number') { 24 | return color; 25 | } 26 | 27 | num >>>= 0; 28 | 29 | const b = num & 0xff, 30 | g = (num & 0xff00) >>> 8, 31 | r = (num & 0xff0000) >>> 16; 32 | 33 | return 'rgba(' + [r, g, b, alpha].join(',') + ')'; 34 | }; 35 | 36 | export const timeAgo = ( 37 | date: Date, 38 | ): { title: I18nKeys; options?: { count: number } } => { 39 | const diff = (new Date().getTime() - date.getTime()) / 1000; 40 | 41 | const day_diff = Math.floor(diff / 86400); 42 | 43 | const conditions: Array<{ 44 | check: boolean; 45 | result: { title: I18nKeys; options?: any }; 46 | }> = [ 47 | { 48 | check: isNaN(day_diff) || day_diff < 0 || day_diff >= 31, 49 | result: { title: 'date:just_now' }, 50 | }, 51 | { check: day_diff === 0 && diff < 60, result: { title: 'date:just_now' } }, 52 | { 53 | check: day_diff === 0 && diff < 120, 54 | result: { options: { count: 1 }, title: 'date:minute_ago' }, 55 | }, 56 | { 57 | check: day_diff === 0 && diff < 3600, 58 | result: { 59 | options: { count: Math.floor(diff / 60) }, 60 | title: 'date:minute_ago', 61 | }, 62 | }, 63 | { 64 | check: day_diff === 0 && diff < 7200, 65 | result: { options: { count: 1 }, title: 'date:hour_ago' }, 66 | }, 67 | { 68 | check: day_diff === 0 && diff < 86400, 69 | result: { 70 | options: { count: Math.floor(diff / 3600) }, 71 | title: 'date:hour_ago', 72 | }, 73 | }, 74 | { check: day_diff === 1, result: { title: 'date:yesterday' } }, 75 | { check: day_diff < 7, result: { title: 'date:last_week' } }, 76 | { check: day_diff < 31, result: { title: 'date:last_month' } }, 77 | { 78 | check: day_diff < 365, 79 | result: { 80 | options: { count: Math.ceil(day_diff / 30) }, 81 | title: 'date:months_ago', 82 | }, 83 | }, 84 | { check: day_diff === 365, result: { title: 'date:last_year' } }, 85 | { 86 | check: true, 87 | result: { 88 | options: { count: Math.floor(day_diff / 365) }, 89 | title: 'date:years_ago', 90 | }, 91 | }, 92 | ]; 93 | 94 | for (const condition of conditions) { 95 | if (condition.check) { 96 | return condition.result; 97 | } 98 | } 99 | 100 | return { 101 | options: { count: Math.floor(day_diff / 365) }, 102 | title: 'date:years_ago', 103 | }; 104 | }; 105 | -------------------------------------------------------------------------------- /template/src/app/common/regex/index.ts: -------------------------------------------------------------------------------- 1 | export const rxEmail = 2 | /^[a-zA-Z0-9]+([%^&=+,.\\-][a-zA-Z0-9]+)*@[a-zA-Z]+(\\.[a-zA-Z]+)*(\\.[a-zA-Z]{2,3})$/g; 3 | 4 | export const rxPassword = 5 | /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*\W)(?!.*['"]).{8,}$/; 6 | 7 | export const rxNumber = /[^\d]+/g; 8 | -------------------------------------------------------------------------------- /template/src/app/common/request-permission/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * remove this line when use 3 | */ 4 | export {}; 5 | /** 6 | import {Platform} from 'react-native'; 7 | import { 8 | PERMISSIONS, 9 | request, 10 | } from 'react-native-permissions'; 11 | 12 | export async function useCameraPermission() { 13 | const status = await request( 14 | Platform.select({ 15 | android: PERMISSIONS.ANDROID.CAMERA, 16 | ios: PERMISSIONS.IOS.CAMERA, 17 | }), 18 | ); 19 | return status; 20 | } 21 | export async function useMediaPermission() { 22 | const statusRead = await request( 23 | Platform.select({ 24 | android: PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE, 25 | ios: PERMISSIONS.IOS.MEDIA_LIBRARY, 26 | }), 27 | ); 28 | const statusWrite = await request( 29 | Platform.select({ 30 | android: PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE, 31 | ios: PERMISSIONS.IOS.MEDIA_LIBRARY, 32 | }), 33 | ); 34 | return {statusRead, statusWrite}; 35 | } 36 | export async function useLocationPermission() { 37 | const status = await request( 38 | Platform.select({ 39 | android: PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION, 40 | ios: PERMISSIONS.IOS.LOCATION_WHEN_IN_USE, 41 | }), 42 | ); 43 | return status; 44 | } 45 | 46 | */ 47 | -------------------------------------------------------------------------------- /template/src/app/common/signal/index.tsx: -------------------------------------------------------------------------------- 1 | import { useAppStore } from '@stores/app'; 2 | 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | class Signal { 5 | private abortController: AbortController; 6 | constructor() { 7 | this.abortController = new AbortController(); 8 | } 9 | 10 | get signal(): AbortController['signal'] { 11 | this.abort(); 12 | 13 | return this.abortController.signal; 14 | } 15 | 16 | abort = () => { 17 | this.abortController.abort(); 18 | 19 | this.abortController = new AbortController(); 20 | }; 21 | } 22 | 23 | type ObjectFromList, V = string> = { 24 | [K in T extends ReadonlyArray ? U : never]: V; 25 | }; 26 | 27 | export function createSignal>( 28 | list: T, 29 | ): ObjectFromList { 30 | const result = list.reduce((prev, curr) => { 31 | prev[curr] = new Signal(); 32 | 33 | return prev; 34 | }, {} as Record); 35 | 36 | return result as ObjectFromList; 37 | } 38 | 39 | export async function withLoadingFunc(callback: () => any) { 40 | useAppStore.getState().setLoading(true); 41 | 42 | await callback(); 43 | 44 | useAppStore.getState().setLoading(false); 45 | } 46 | -------------------------------------------------------------------------------- /template/src/app/common/social-login/facebook.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | /** 3 | import { 4 | AccessToken, 5 | GraphRequest, 6 | GraphRequestManager, 7 | LoginManager, 8 | Settings, 9 | } from 'react-native-fbsdk-next'; 10 | 11 | const init = () => { 12 | Settings.initializeSDK(); 13 | }; 14 | 15 | type LoginResult = {success: boolean; token?: string}; 16 | const login = () => { 17 | return new Promise(rs => { 18 | LoginManager.logInWithPermissions(['public_profile', 'email']).then( 19 | result => { 20 | if (result.isCancelled) { 21 | rs({success: false}); 22 | } else { 23 | AccessToken.getCurrentAccessToken() 24 | .then(data => { 25 | if (data && data.accessToken) { 26 | rs({success: true, token: data.accessToken}); 27 | } else { 28 | rs({success: false}); 29 | } 30 | }) 31 | .catch(err => { 32 | console.log('FACEBOOK-LOGIN-ERROR', err); 33 | rs({success: false}); 34 | }); 35 | } 36 | }, 37 | ); 38 | }); 39 | }; 40 | 41 | const logout = async () => { 42 | try { 43 | const data = await AccessToken.getCurrentAccessToken(); 44 | if (data?.accessToken) { 45 | const logoutGraph = new GraphRequest( 46 | 'me/permissions/', 47 | { 48 | accessToken: data?.accessToken, 49 | httpMethod: 'DELETE', 50 | }, 51 | error => { 52 | if (error) { 53 | console.log('Error fetching data: ' + error.toString()); 54 | } else { 55 | LoginManager.logOut(); 56 | } 57 | }, 58 | ); 59 | new GraphRequestManager().addRequest(logoutGraph).start(); 60 | } else { 61 | LoginManager.logOut(); 62 | } 63 | } catch (err) { 64 | console.log('FACEBOOK-LOGOUT-ERROR', err); 65 | } 66 | }; 67 | 68 | export const FacebookService = { 69 | init, 70 | login, 71 | logout, 72 | }; 73 | 74 | */ 75 | -------------------------------------------------------------------------------- /template/src/app/common/social-login/google.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | /** 3 | import {GoogleSignin} from '@react-native-google-signin/google-signin'; 4 | 5 | const init = () => { 6 | GoogleSignin.configure(); 7 | }; 8 | type LoginResult = {success: boolean; token?: string}; 9 | const login = async (): Promise => { 10 | try { 11 | await GoogleSignin.hasPlayServices(); 12 | await GoogleSignin.signIn(); 13 | const userToken = await GoogleSignin.getTokens(); 14 | return {success: true, token: userToken.accessToken}; 15 | } catch (err) { 16 | console.log('GOOGLE-LOGIN-ERROR', err); 17 | return {success: false}; 18 | } 19 | }; 20 | 21 | const logout = async () => { 22 | try { 23 | await GoogleSignin.signOut(); 24 | return true; 25 | } catch (err) { 26 | console.log('GOOGLE-LOGOUT-ERROR', err); 27 | } 28 | }; 29 | 30 | export const GoogleService = { 31 | init, 32 | login, 33 | logout, 34 | }; 35 | 36 | */ 37 | -------------------------------------------------------------------------------- /template/src/app/common/social-login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apple'; 2 | 3 | export * from './facebook'; 4 | 5 | export * from './google'; 6 | -------------------------------------------------------------------------------- /template/src/app/common/socketIo/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * remove this line when use 3 | */ 4 | export {}; 5 | /** 6 | import {createContext, useCallback, useContext, useState} from 'react'; 7 | import {io, Socket} from 'socket.io-client'; 8 | 9 | export const useSocket = () => { 10 | // state 11 | const [socket, setSocket] = useState(undefined); 12 | 13 | // function 14 | const socketDisconnect = useCallback(() => { 15 | if (socket) { 16 | socket.offAny(); 17 | socket.disconnect(); 18 | setSocket(undefined); 19 | } 20 | }, [socket]); 21 | 22 | const socketInit = useCallback(() => { 23 | const client = io('', { 24 | transports: ['websocket'], 25 | reconnection: true, 26 | reconnectionDelay: 500, 27 | reconnectionAttempts: 9999999, 28 | forceNew: true, 29 | }); 30 | client.on('connection-success', () => { 31 | console.log('Connected', client.connected); 32 | }); 33 | setSocket(client); 34 | }, []); 35 | 36 | const socketOff = useCallback( 37 | (event?: string, listener?: any) => { 38 | if (socket) { 39 | socket.off(event, listener); 40 | } 41 | }, 42 | [socket], 43 | ); 44 | 45 | const socketListen = useCallback( 46 | (event: string, listener: (...args: any[]) => void) => { 47 | if (socket) { 48 | socket.on(event, listener); 49 | } 50 | }, 51 | [socket], 52 | ); 53 | 54 | // result 55 | return {socket, socketInit, socketOff, socketListen, socketDisconnect}; 56 | }; 57 | 58 | type SocketContext = { 59 | socket: Socket | undefined; 60 | socketOff: (event?: string, listener?: any) => void; 61 | socketListen: (event: string, listener: (...args: any[]) => void) => void; 62 | socketInit: () => void; 63 | socketDisconnect: () => void; 64 | }; 65 | 66 | export const SocketIoContext = createContext( 67 | {} as SocketContext, 68 | ); 69 | export const SocketProvider = SocketIoContext.Provider; 70 | export const useSocketContext = () => useContext(SocketIoContext); 71 | 72 | */ 73 | -------------------------------------------------------------------------------- /template/src/app/common/string/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export const trimArray = (sourceArr: Array = []): Array => { 3 | return sourceArr.map((element: any) => { 4 | if (Array.isArray(element)) { 5 | return trimArray(element); 6 | } 7 | 8 | switch (typeof element) { 9 | case 'string': 10 | return element.trim(); 11 | case 'object': 12 | return trimObject(element); 13 | 14 | default: 15 | return element; 16 | } 17 | }); 18 | }; 19 | 20 | export const trimObject = (source: any) => { 21 | if (!source) { 22 | return source; 23 | } 24 | 25 | const newObject = source; 26 | 27 | Object.keys(newObject).forEach((key: string) => { 28 | if (Array.isArray(newObject[key])) { 29 | newObject[key] = trimArray(newObject[key]); 30 | } 31 | 32 | if (typeof newObject[key] === 'string') { 33 | newObject[key] = newObject[key].trim(); 34 | } 35 | 36 | if (typeof newObject[key] === 'object') { 37 | newObject[key] = trimObject(newObject[key]); 38 | } 39 | }); 40 | 41 | return newObject; 42 | }; 43 | 44 | export const checkPasswordContainUserName = ( 45 | username: string, 46 | password: string, 47 | ) => { 48 | const numConsecutiveChars = 3; 49 | 50 | // first find all combinations that should not be found in password 51 | const invalidCombinations = []; 52 | 53 | for (let i = 0; i < username.length - numConsecutiveChars; i++) { 54 | const curCombination = username[i] + username[i + 1] + username[i + 2]; 55 | 56 | invalidCombinations.push(curCombination); 57 | } 58 | 59 | // now check all invalidCombinations 60 | let invalid = false; 61 | for (const curCombination of invalidCombinations) { 62 | if (password.indexOf(curCombination) !== -1) { 63 | invalid = true; 64 | 65 | break; 66 | } 67 | } 68 | 69 | return invalid; 70 | }; 71 | 72 | /** 73 | * @param keyT key of i18n 74 | * @param options object translate parameter 75 | * @param optionsTx object translate parameter will translate before set to option base. see detail bellow 76 | * ex: json file : {"field":{"email":"Email"},"msg":{"msg1":"{{fieldName}} is required"}} 77 | * => optionsTx = {fieldName:"field:email"} 78 | * fieldName must translate with i18n 79 | * so fieldName option will be push on optionsTx 80 | * This will support translate Option on translate 81 | * Read hook useErrorMessageTranslation 82 | */ 83 | export const stringifyObjectValidate = ({ 84 | keyT, 85 | options, 86 | optionsTx, 87 | }: ValidateMessageObject) => { 88 | return JSON.stringify({ 89 | keyT, 90 | options, 91 | optionsTx, 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /template/src/app/common/zod-validate/login.ts: -------------------------------------------------------------------------------- 1 | import { FormLoginType } from '@model/authentication'; 2 | import { z } from 'zod'; 3 | 4 | import { stringifyObjectValidate } from '../string/index'; 5 | 6 | export const loginValidation = z.object>({ 7 | email: z 8 | .string() 9 | .min( 10 | 1, 11 | stringifyObjectValidate({ 12 | keyT: 'validation:email_required', 13 | }), 14 | ) 15 | .email(), 16 | password: z.string().min(1, 'Password is required'), 17 | }); 18 | -------------------------------------------------------------------------------- /template/src/app/library/components/button/default-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableOpacity, TouchableOpacityProps } from 'react-native'; 3 | 4 | import { useThrottle } from './hook'; 5 | import { ButtonProps } from './type'; 6 | 7 | export const DefaultButton = ({ 8 | throttleMs, 9 | onPress, 10 | onPressIn, 11 | onPressOut, 12 | onLongPress, 13 | children, 14 | ...rest 15 | }: TouchableOpacityProps & Pick) => { 16 | const [, handlePress, handleLongPress, handlePressIn, handlePressOut] = 17 | useThrottle({ 18 | onLongPress, 19 | onPress, 20 | onPressIn, 21 | onPressOut, 22 | throttleMs, 23 | }); 24 | 25 | // render 26 | return ( 27 | 33 | {children} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /template/src/app/library/components/button/hook.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GestureResponderEvent, 3 | TouchableWithoutFeedbackProps, 4 | } from 'react-native'; 5 | 6 | import { Easing, useSharedValue, withTiming } from 'react-native-reanimated'; 7 | 8 | import { useEventCallback } from '@hooks'; 9 | 10 | export type UseThrottleParam = { 11 | throttleMs?: number; 12 | onPress?: TouchableWithoutFeedbackProps['onPress']; 13 | onLongPress?: TouchableWithoutFeedbackProps['onLongPress']; 14 | onPressIn?: TouchableWithoutFeedbackProps['onPressIn']; 15 | onPressOut?: TouchableWithoutFeedbackProps['onPressOut']; 16 | }; 17 | 18 | export const useThrottle = ({ 19 | throttleMs = 200, 20 | onPress, 21 | onPressIn, 22 | onPressOut, 23 | onLongPress, 24 | }: UseThrottleParam) => { 25 | // state 26 | const progressToEnable = useSharedValue(0); 27 | 28 | const pressed = useSharedValue(false); 29 | 30 | // func 31 | const handlePress = useEventCallback((e: GestureResponderEvent) => { 32 | if (isTypeof(onPress, 'function')) { 33 | if (progressToEnable.value > 0) { 34 | return; 35 | } 36 | 37 | if (isTypeof(throttleMs, 'number')) { 38 | progressToEnable.value = 1; 39 | 40 | progressToEnable.value = withTiming(0, { 41 | duration: throttleMs, 42 | easing: Easing.linear, 43 | }); 44 | } 45 | 46 | execFunc(onPress, e); 47 | } 48 | }); 49 | 50 | const handleLongPress = useEventCallback((e: GestureResponderEvent) => { 51 | if (isTypeof(onLongPress, 'function')) { 52 | if (progressToEnable.value > 0) { 53 | return; 54 | } 55 | 56 | if (isTypeof(throttleMs, 'number')) { 57 | progressToEnable.value = 1; 58 | 59 | progressToEnable.value = withTiming(0, { 60 | duration: throttleMs, 61 | easing: Easing.linear, 62 | }); 63 | } 64 | 65 | execFunc(onLongPress, e); 66 | } 67 | }); 68 | 69 | const handlePressIn = useEventCallback((e: GestureResponderEvent) => { 70 | if (progressToEnable.value > 0) { 71 | return; 72 | } 73 | 74 | pressed.value = true; 75 | 76 | execFunc(onPressIn, e); 77 | }); 78 | 79 | const handlePressOut = useEventCallback((e: GestureResponderEvent) => { 80 | if (progressToEnable.value > 0) { 81 | return; 82 | } 83 | 84 | pressed.value = false; 85 | 86 | execFunc(onPressOut, e); 87 | }); 88 | 89 | // result 90 | return [ 91 | progressToEnable, 92 | handlePress, 93 | handleLongPress, 94 | handlePressIn, 95 | handlePressOut, 96 | pressed, 97 | ] as const; 98 | }; 99 | -------------------------------------------------------------------------------- /template/src/app/library/components/button/outline-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ImageProps, TouchableWithoutFeedback } from 'react-native'; 3 | 4 | import { useTranslation } from 'react-i18next'; 5 | import { 6 | useAnimatedProps, 7 | useAnimatedStyle, 8 | useDerivedValue, 9 | } from 'react-native-reanimated'; 10 | import { useStyles } from 'react-native-unistyles'; 11 | 12 | import { AnimatedIcon } from '@components/icon'; 13 | import { AnimatedText, View } from '@rn-core'; 14 | 15 | import { useThrottle } from './hook'; 16 | import { buttonStyleSheet } from './styles'; 17 | import { ButtonProps } from './type'; 18 | 19 | export const OutlineButton = ({ 20 | t18n, 21 | text, 22 | throttleMs, 23 | onPress, 24 | onPressIn, 25 | onPressOut, 26 | onLongPress, 27 | leftIcon, 28 | rightIcon, 29 | size = 'normal', 30 | disabled = false, 31 | ...rest 32 | }: ButtonProps) => { 33 | // state 34 | const { 35 | styles, 36 | theme: { color }, 37 | } = useStyles(buttonStyleSheet); 38 | 39 | const [t] = useTranslation(); 40 | 41 | const [ 42 | , 43 | handlePress, 44 | handleLongPress, 45 | handlePressIn, 46 | handlePressOut, 47 | pressed, 48 | ] = useThrottle({ 49 | onLongPress, 50 | onPress, 51 | onPressIn, 52 | onPressOut, 53 | throttleMs, 54 | }); 55 | 56 | const tintColor = useDerivedValue(() => { 57 | if (disabled) { 58 | return color.neutral100; 59 | } 60 | 61 | if (pressed.value) { 62 | return color.primary; 63 | } 64 | 65 | return color.primary500; 66 | }); 67 | 68 | // style 69 | const textStyle = useAnimatedStyle(() => { 70 | return { 71 | color: tintColor.value, 72 | }; 73 | }); 74 | 75 | // props 76 | const iconProps = useAnimatedProps(() => { 77 | return { 78 | tintColor: tintColor.value, 79 | }; 80 | }); 81 | 82 | // render 83 | return ( 84 | 91 | 95 | {leftIcon ? ( 96 | 97 | ) : null} 98 | {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} 99 | {/* @ts-ignore */} 100 | 101 | {t18n ? t(t18n) : text} 102 | 103 | {rightIcon ? ( 104 | 105 | ) : null} 106 | 107 | 108 | ); 109 | }; 110 | -------------------------------------------------------------------------------- /template/src/app/library/components/button/primary-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableWithoutFeedback } from 'react-native'; 3 | 4 | import { useTranslation } from 'react-i18next'; 5 | import { useAnimatedStyle } from 'react-native-reanimated'; 6 | import { useStyles } from 'react-native-unistyles'; 7 | 8 | import { Icon } from '@components/icon'; 9 | import { AnimatedView, Text } from '@rn-core'; 10 | import { Colors } from '@theme/index'; 11 | 12 | import { useThrottle } from './hook'; 13 | import { buttonStyleSheet } from './styles'; 14 | import { ButtonProps } from './type'; 15 | 16 | export const PrimaryButton = ({ 17 | t18n, 18 | text, 19 | throttleMs, 20 | onPress, 21 | onPressIn, 22 | onPressOut, 23 | onLongPress, 24 | leftIcon, 25 | rightIcon, 26 | size = 'normal', 27 | disabled = false, 28 | ...rest 29 | }: ButtonProps) => { 30 | // state 31 | const { 32 | styles, 33 | theme: { color }, 34 | } = useStyles(buttonStyleSheet); 35 | 36 | const [t] = useTranslation(); 37 | 38 | const [ 39 | , 40 | handlePress, 41 | handleLongPress, 42 | handlePressIn, 43 | handlePressOut, 44 | pressed, 45 | ] = useThrottle({ 46 | onLongPress, 47 | onPress, 48 | onPressIn, 49 | onPressOut, 50 | throttleMs, 51 | }); 52 | 53 | // func 54 | const iconColor: Colors = disabled ? 'neutral200' : 'neutral50'; 55 | 56 | // style 57 | const containerStyle = useAnimatedStyle(() => { 58 | let backgroundColor: string = color.primary500; 59 | if (disabled) { 60 | backgroundColor = color.neutral100; 61 | } else if (pressed.value) { 62 | backgroundColor = color.primary; 63 | } 64 | 65 | return { 66 | backgroundColor, 67 | }; 68 | }); 69 | 70 | // render 71 | return ( 72 | 79 | 83 | {leftIcon ? : null} 84 | {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} 85 | {/* @ts-ignore */} 86 | 87 | {t18n ? t(t18n) : text} 88 | 89 | {rightIcon ? : null} 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /template/src/app/library/components/button/styles.ts: -------------------------------------------------------------------------------- 1 | import { createStyleSheet } from 'react-native-unistyles'; 2 | 3 | export const buttonStyleSheet = createStyleSheet(({ color, textPresets }) => ({ 4 | buttonColor: (disabled?: boolean) => ({ 5 | backgroundColor: disabled ? color.neutral200 : color.neutral50, 6 | borderColor: disabled ? color.neutral200 : color.primary500, 7 | }), 8 | extraSmall: { 9 | alignItems: 'center', 10 | borderRadius: 8, 11 | columnGap: 8, 12 | flexDirection: 'row', 13 | overflow: 'hidden', 14 | padding: 8, 15 | }, 16 | normal: { 17 | alignItems: 'center', 18 | borderRadius: 8, 19 | columnGap: 8, 20 | flexDirection: 'row', 21 | overflow: 'hidden', 22 | padding: 12, 23 | }, 24 | outline: { 25 | borderWidth: 1, 26 | }, 27 | small: { 28 | alignItems: 'center', 29 | borderRadius: 8, 30 | columnGap: 8, 31 | flexDirection: 'row', 32 | overflow: 'hidden', 33 | padding: 10, 34 | }, 35 | textColor: (disabled?: boolean) => ({ 36 | color: disabled ? color.neutral200 : color.neutral50, 37 | }), 38 | text_extraSmall: { ...textPresets.extraSmall }, 39 | text_normal: { ...textPresets.CTAs }, 40 | text_small: { ...textPresets.CTASmall }, 41 | })); 42 | -------------------------------------------------------------------------------- /template/src/app/library/components/button/type.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TouchableWithoutFeedbackProps } from 'react-native'; 3 | 4 | import { IconTypes } from '@assets/icon'; 5 | 6 | export type ButtonProps = RequireAtLeastOne< 7 | { 8 | /** 9 | * Button size 10 | * @default normal 11 | */ 12 | size?: 'normal' | 'small' | 'extraSmall'; 13 | 14 | /** 15 | * Children for button 16 | * @default undefined 17 | */ 18 | children?: React.ReactNode; 19 | 20 | /** 21 | * Left Icon 22 | */ 23 | leftIcon?: IconTypes; 24 | 25 | /** 26 | * Right Icon 27 | */ 28 | rightIcon?: IconTypes; 29 | 30 | /** 31 | * Disable button when press 32 | */ 33 | throttleMs?: number; 34 | 35 | text: string; 36 | 37 | t18n: I18nKeys; 38 | }, 39 | 't18n' | 'text' 40 | > & 41 | TouchableWithoutFeedbackProps; 42 | -------------------------------------------------------------------------------- /template/src/app/library/components/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { ImageProps, TouchableWithoutFeedback } from 'react-native'; 3 | 4 | import { 5 | interpolateColor, 6 | useAnimatedProps, 7 | useAnimatedStyle, 8 | } from 'react-native-reanimated'; 9 | import { createStyleSheet, useStyles } from 'react-native-unistyles'; 10 | 11 | import { useSharedTransition } from '@animated'; 12 | import { AnimatedIcon } from '@components/icon'; 13 | import { AnimatedView } from '@rn-core'; 14 | 15 | import { CheckboxProps } from './type'; 16 | 17 | export const Checkbox = ({ 18 | value, 19 | initialValue = false, 20 | onToggle, 21 | size = 24, 22 | disabled = false, 23 | }: CheckboxProps) => { 24 | // state 25 | const { 26 | styles, 27 | theme: { color }, 28 | } = useStyles(stylesSheet); 29 | 30 | const [localValue, setLocalValue] = useState(initialValue); 31 | 32 | const progress = useSharedTransition( 33 | isTypeof(value, 'boolean') ? value : localValue, 34 | { duration: 200 }, 35 | ); 36 | 37 | // func 38 | const onPress = () => { 39 | if (typeof value === 'boolean') { 40 | execFunc(onToggle, !value); 41 | } else { 42 | execFunc(onToggle, !localValue); 43 | 44 | setLocalValue(v => !v); 45 | } 46 | }; 47 | 48 | // style 49 | const containerStyle = useAnimatedStyle(() => { 50 | return { 51 | backgroundColor: disabled 52 | ? color.neutral50 53 | : interpolateColor( 54 | progress.value, 55 | [0, 1], 56 | [color.neutral50, color.primary500], 57 | ), 58 | borderColor: disabled 59 | ? color.neutral200 60 | : interpolateColor( 61 | progress.value, 62 | [0, 1], 63 | [color.technical, color.primary500], 64 | ), 65 | }; 66 | }); 67 | 68 | const iconProps = useAnimatedProps(() => ({ 69 | tintColor: disabled 70 | ? color.neutral200 71 | : interpolateColor( 72 | progress.value, 73 | [0, 1], 74 | ['transparent', color.neutral50], 75 | ), 76 | })); 77 | 78 | // render 79 | return ( 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | }; 87 | 88 | const stylesSheet = createStyleSheet({ 89 | container: (size: number) => ({ 90 | alignItems: 'center', 91 | borderRadius: 4, 92 | borderWidth: 1, 93 | height: size, 94 | justifyContent: 'center', 95 | position: 'relative', 96 | width: size, 97 | }), 98 | }); 99 | -------------------------------------------------------------------------------- /template/src/app/library/components/checkbox/type.ts: -------------------------------------------------------------------------------- 1 | export type CheckboxProps = { 2 | /** 3 | * Default state of checkbox 4 | * @default false 5 | */ 6 | initialValue?: boolean; 7 | 8 | /** 9 | * checkbox button size 10 | * @default 24 11 | */ 12 | size?: number; 13 | 14 | /** 15 | * Overwrite value 16 | * @default undefined 17 | */ 18 | value?: boolean; 19 | 20 | /** 21 | * On checkbox button press 22 | */ 23 | onToggle?: (value: boolean) => void; 24 | 25 | /** 26 | * checkbox button is disabled 27 | * @default false 28 | */ 29 | disabled?: boolean; 30 | }; 31 | -------------------------------------------------------------------------------- /template/src/app/library/components/core/Text.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | 3 | import { NativeText } from 'react-native/Libraries/Text/TextNativeComponent'; 4 | 5 | export const Text = forwardRef((props, ref) => { 6 | return ( 7 | // eslint-disable-next-line react/jsx-filename-extension 8 | 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /template/src/app/library/components/core/View.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | import type * as React from 'react'; 11 | import { NativeMethods, ViewProps } from 'react-native'; 12 | 13 | import { Constructor } from 'react-native/types/private/Utilities'; 14 | 15 | /** 16 | * The most fundamental component for building UI, View is a container that supports layout with flexbox, style, some touch handling, 17 | * and accessibility controls, and is designed to be nested inside other views and to have 0 to many children of any type. 18 | * View maps directly to the native view equivalent on whatever platform React is running on, 19 | * whether that is a UIView,
, android.view, etc. 20 | */ 21 | declare class ViewComponent extends React.Component {} 22 | declare const ViewBase: Constructor & typeof ViewComponent; 23 | 24 | export class View extends ViewBase { 25 | /** 26 | * Is 3D Touch / Force Touch available (i.e. will touch events include `force`) 27 | * @platform ios 28 | */ 29 | static forceTouchAvailable: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /template/src/app/library/components/core/View.js: -------------------------------------------------------------------------------- 1 | import NativeView from 'react-native/Libraries/Components/View/ViewNativeComponent'; 2 | 3 | const View = NativeView; 4 | 5 | View.displayName = 'View'; 6 | 7 | export { View }; 8 | -------------------------------------------------------------------------------- /template/src/app/library/components/core/index.ts: -------------------------------------------------------------------------------- 1 | import Animated from 'react-native-reanimated'; 2 | 3 | import { Text } from './Text'; 4 | import { View } from './View'; 5 | 6 | export const AnimatedText = Animated.createAnimatedComponent(Text); 7 | 8 | export const AnimatedView = Animated.createAnimatedComponent(View); 9 | 10 | export { Text, View }; 11 | -------------------------------------------------------------------------------- /template/src/app/library/components/divider/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { StyleSheet, ViewStyle } from 'react-native'; 3 | 4 | import { useStyles } from 'react-native-unistyles'; 5 | 6 | import { View } from '@rn-core'; 7 | 8 | import { DividerProps } from './type'; 9 | 10 | export const Divider = ({ 11 | height = 1, 12 | colorTheme = 'neutral200', 13 | }: DividerProps) => { 14 | // state 15 | const { theme } = useStyles(); 16 | 17 | // style 18 | const divider = useMemo( 19 | () => ({ 20 | backgroundColor: 21 | colorTheme && typeof theme.color[colorTheme] === 'string' 22 | ? (theme.color[colorTheme] as string) 23 | : undefined, 24 | height: height * StyleSheet.hairlineWidth, 25 | width: '100%', 26 | }), 27 | [colorTheme, height, theme.color], 28 | ); 29 | 30 | // render 31 | return ; 32 | }; 33 | -------------------------------------------------------------------------------- /template/src/app/library/components/divider/type.ts: -------------------------------------------------------------------------------- 1 | import { Colors } from '@theme/index'; 2 | 3 | export interface DividerProps { 4 | /** 5 | * Overwrite color with theme 6 | */ 7 | colorTheme?: Colors; 8 | 9 | /** 10 | * Height of divider 11 | * @default 1 12 | */ 13 | height?: number; 14 | } 15 | -------------------------------------------------------------------------------- /template/src/app/library/components/focus-aware-status-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { useIsFocused } from '@react-navigation/native'; 5 | import { View } from '@rn-core'; 6 | import { StatusBar, StatusBarProps } from 'expo-status-bar'; 7 | 8 | export const FocusAwareStatusBar = ({ 9 | style = 'dark', 10 | ...props 11 | }: StatusBarProps) => { 12 | // state 13 | const isFocused = useIsFocused(); 14 | 15 | // render 16 | return isFocused ? ( 17 | 18 | 19 | 20 | ) : null; 21 | }; 22 | 23 | const styles = StyleSheet.create({ 24 | container: { 25 | opacity: 0, 26 | position: 'absolute', 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /template/src/app/library/components/icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useMemo } from 'react'; 2 | import { Image, ImageProps, ImageStyle } from 'react-native'; 3 | 4 | import Animated, { AnimatedProps } from 'react-native-reanimated'; 5 | import { useStyles } from 'react-native-unistyles'; 6 | 7 | import { icons } from '@assets/icon'; 8 | 9 | import { IconProps } from './type'; 10 | 11 | const AnimatedImage = Animated.createAnimatedComponent(Image); 12 | 13 | const SIZE = 24; 14 | 15 | export const Icon = ({ 16 | icon, 17 | colorTheme, 18 | size = SIZE, 19 | resizeMode = 'contain', 20 | }: IconProps & Pick) => { 21 | // state 22 | const { theme } = useStyles(); 23 | 24 | // style 25 | const style = useMemo( 26 | () => ({ height: size, width: size }), 27 | [size], 28 | ); 29 | 30 | // render 31 | return ( 32 | 42 | ); 43 | }; 44 | 45 | export const AnimatedIcon = forwardRef< 46 | Image, 47 | AnimatedProps & IconProps 48 | >( 49 | ( 50 | { 51 | icon, 52 | colorTheme, 53 | size = SIZE, 54 | resizeMode = 'contain', 55 | ...rest 56 | }: Partial> & IconProps, 57 | ref, 58 | ) => { 59 | // state 60 | const { theme } = useStyles(); 61 | 62 | // style 63 | const style = useMemo( 64 | () => ({ height: size, width: size }), 65 | [size], 66 | ); 67 | 68 | // render 69 | return ( 70 | 82 | ); 83 | }, 84 | ); 85 | -------------------------------------------------------------------------------- /template/src/app/library/components/icon/type.ts: -------------------------------------------------------------------------------- 1 | import { IconTypes } from '@assets/icon'; 2 | import { Colors } from '@theme/index'; 3 | 4 | export interface IconProps { 5 | /** 6 | * Size of Icon 7 | * @default 24 8 | */ 9 | size?: number; 10 | 11 | /** 12 | * Overwrite tint color with theme 13 | */ 14 | colorTheme?: Colors; 15 | 16 | /** 17 | * Icon type 18 | * @default undefined 19 | */ 20 | icon: IconTypes; 21 | } 22 | -------------------------------------------------------------------------------- /template/src/app/library/components/image/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet } from 'react-native'; 3 | 4 | import { Image as ExpoImage } from 'expo-image'; 5 | 6 | import { ImageProps } from './type'; 7 | 8 | export const Image = ({ ...rest }: ImageProps) => { 9 | // render 10 | return ( 11 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /template/src/app/library/components/image/type.ts: -------------------------------------------------------------------------------- 1 | import { ImageProps as ExpoImageProps } from 'expo-image'; 2 | 3 | export type ImageProps = Omit; 4 | -------------------------------------------------------------------------------- /template/src/app/library/components/list-view/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, RefreshControl } from 'react-native'; 3 | 4 | import { FlashList } from '@shopify/flash-list'; 5 | 6 | import { ListViewProps } from './type'; 7 | 8 | export const ListView = (props: ListViewProps) => { 9 | // state 10 | const { 11 | type = 'flashlist', 12 | onRefresh, 13 | onLoadMore, 14 | canRefresh = false, 15 | canLoadMore = false, 16 | refreshing = false, 17 | } = props; 18 | 19 | // function 20 | const loadMore = () => { 21 | if (canLoadMore) { 22 | execFunc(onLoadMore); 23 | } 24 | }; 25 | 26 | const ListComponent = type === 'flashlist' ? FlashList : FlatList; 27 | 28 | // render 29 | return ( 30 | 34 | ) : undefined 35 | } 36 | onEndReached={loadMore} 37 | onEndReachedThreshold={0.001} 38 | showsVerticalScrollIndicator={false} 39 | showsHorizontalScrollIndicator={false} 40 | {...props} 41 | onRefresh={undefined} 42 | refreshing={undefined} 43 | /> 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /template/src/app/library/components/list-view/type.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { FlatListProps } from 'react-native'; 3 | 4 | import { FlashListProps } from '@shopify/flash-list'; 5 | 6 | export type ListViewProps = ( 7 | | ({ 8 | type: 'flatlist'; 9 | } & ReOmit< 10 | FlatListProps, 11 | 'onRefresh' | 'refreshControl' | 'refreshing' 12 | >) 13 | | ({ type?: 'flashlist' } & ReOmit< 14 | FlashListProps, 15 | 'onRefresh' | 'refreshControl' | 'refreshing' 16 | >) 17 | ) & { 18 | /** 19 | * Function when refreshing 20 | * @default undefined 21 | */ 22 | onRefresh?: () => void; 23 | 24 | /** 25 | * Function when scroll to end 26 | * @default undefined 27 | */ 28 | onLoadMore?: () => void; 29 | 30 | /** 31 | * Enable to load more when scroll to end 32 | * @default false 33 | */ 34 | canLoadMore?: boolean; 35 | 36 | /** 37 | * State of Refresh Control 38 | * @default false 39 | */ 40 | refreshing?: boolean; 41 | 42 | /** 43 | * Enable to render Refresh Control 44 | * @default true 45 | */ 46 | canRefresh?: boolean; 47 | }; 48 | -------------------------------------------------------------------------------- /template/src/app/library/components/local-image/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, ImageStyle, StyleSheet } from 'react-native'; 3 | 4 | import { images } from '@assets/image'; 5 | import { View } from '@rn-core'; 6 | 7 | import { LocalImageProps } from './type'; 8 | 9 | export const LocalImage = ({ 10 | source, 11 | containerStyle, 12 | style: styleOverride, 13 | resizeMode = 'cover', 14 | }: LocalImageProps) => { 15 | // render 16 | return ( 17 | 18 | 23 | 24 | ); 25 | }; 26 | 27 | const styles = StyleSheet.create({ 28 | img: { 29 | height: '100%', 30 | width: '100%', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /template/src/app/library/components/local-image/type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ImageResizeMode, 3 | ImageStyle, 4 | StyleProp, 5 | ViewStyle, 6 | } from 'react-native'; 7 | 8 | import { ImageTypes } from '@assets/image'; 9 | 10 | export interface LocalImageProps { 11 | /** 12 | * Overwrite image style 13 | * @default undefined 14 | */ 15 | style?: ImageStyle; 16 | 17 | /** 18 | * Overwrite wrap image style 19 | * @default undefined 20 | */ 21 | containerStyle?: StyleProp; 22 | 23 | /** 24 | * Source image(local) 25 | * @default undefined 26 | */ 27 | source: ImageTypes; 28 | 29 | /** 30 | * Custom resizeMode 31 | * @default contain 32 | */ 33 | resizeMode?: ImageResizeMode; 34 | } 35 | -------------------------------------------------------------------------------- /template/src/app/library/components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | 3 | import { Portal } from '@gorhom/portal'; 4 | import { useDismissKeyboard } from '@hooks'; 5 | 6 | import { ModalContent } from './modal-content'; 7 | import { ModalProps } from './type'; 8 | 9 | export const Modal = (props: ModalProps) => { 10 | // state 11 | const [visible, setVisible] = useState(props.isVisible); 12 | 13 | const modalContent = useRef(null); 14 | 15 | // function 16 | const closeModal = () => { 17 | setVisible(false); 18 | }; 19 | 20 | // effect 21 | useDismissKeyboard(visible); 22 | 23 | useEffect(() => { 24 | if (props.isVisible) { 25 | setVisible(true); 26 | } else { 27 | modalContent.current?.dismiss(); 28 | } 29 | }, [props.isVisible]); 30 | 31 | // render 32 | return ( 33 | 34 | {visible ? ( 35 | 36 | ) : null} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /template/src/app/library/components/modal/type.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react'; 2 | import { ViewStyle } from 'react-native'; 3 | 4 | import { ComplexAnimationBuilder } from 'react-native-reanimated'; 5 | 6 | export type Direction = 'up' | 'down' | 'left' | 'right'; 7 | 8 | export interface ModalProps { 9 | /** 10 | * Content of modal 11 | * @default undefined 12 | */ 13 | children?: ReactElement; 14 | 15 | /** 16 | * Show/hide modal 17 | * @requires 18 | */ 19 | isVisible: boolean; 20 | 21 | /** 22 | * Custom back drop opacity 23 | * @default 0.3 24 | */ 25 | backdropOpacity?: number; 26 | 27 | /** 28 | * Custom backdrop color 29 | * @default black 30 | */ 31 | backdropColor?: string; 32 | 33 | /** 34 | * Custom backdrop component 35 | */ 36 | customBackDrop?: ReactElement; 37 | 38 | /** 39 | * Modal show animation 40 | * @default fadeIn 41 | */ 42 | entering?: typeof ComplexAnimationBuilder | ComplexAnimationBuilder; 43 | /** 44 | * Modal hide animation 45 | * @default fadeOut 46 | */ 47 | 48 | exiting?: typeof ComplexAnimationBuilder | ComplexAnimationBuilder; 49 | 50 | /** 51 | * Overwrite modal style 52 | * @default undefined 53 | */ 54 | style?: ViewStyle | ViewStyle[]; 55 | 56 | /** 57 | * Called before the modal hide animation begins 58 | * @default undefined 59 | */ 60 | onModalWillHide?: () => void; 61 | 62 | /** 63 | * Called when the modal is completely hidden 64 | * @default undefined 65 | */ 66 | onModalHide?: () => void; 67 | 68 | /** 69 | * Called before the modal show animation begins 70 | * @default undefined 71 | */ 72 | onModalWillShow?: () => void; 73 | 74 | /** 75 | * Called when the modal is completely visible 76 | * @default undefined 77 | */ 78 | onModalShow?: () => void; 79 | 80 | /** 81 | * Called when the backdrop is pressed 82 | * @default undefined 83 | */ 84 | onBackdropPress?: () => void; 85 | 86 | /** 87 | * Called when the Android back button is pressed 88 | * @default undefined 89 | */ 90 | onBackButtonPress?: () => void; 91 | } 92 | -------------------------------------------------------------------------------- /template/src/app/library/components/parsed-text/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | import { ParsedTextProps } from './type'; 5 | import { textExtraction } from './utils'; 6 | 7 | export const ParsedText = ({ parse, children, ...rest }: ParsedTextProps) => { 8 | // function 9 | const renderTexts = useCallback(() => { 10 | if (!parse || !isTypeof(children, 'string')) { 11 | return children; 12 | } 13 | 14 | const text = textExtraction(children, parse); 15 | 16 | return text.map((localProps, index) => { 17 | const { style, ...restText } = localProps; 18 | 19 | return ( 20 | 21 | ); 22 | }); 23 | }, [children, parse]); 24 | 25 | // render 26 | return {renderTexts()}; 27 | }; 28 | -------------------------------------------------------------------------------- /template/src/app/library/components/parsed-text/type.ts: -------------------------------------------------------------------------------- 1 | import { TextProps } from 'react-native'; 2 | 3 | export type ParsedText = { children: string; _matched?: boolean }; 4 | 5 | export type ParsedTexts = Array; 6 | 7 | export type CustomTextProps = ReOmit; 8 | 9 | export type MatchedPart = TextProps & { 10 | _matched: boolean; 11 | }; 12 | 13 | export type Pattern = { 14 | pattern?: RegExp; 15 | lastIndex?: number; 16 | renderText?: (text: string) => string; 17 | }; 18 | 19 | export type Parse = { 20 | pattern?: RegExp; 21 | } & CustomTextProps & { 22 | onPress?: (text: string, index: number) => void; 23 | renderText?: (text: string) => string; 24 | }; 25 | 26 | export interface ParsedTextProps extends TextProps { 27 | parse: Array; 28 | } 29 | -------------------------------------------------------------------------------- /template/src/app/library/components/parsed-text/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import { CustomTextProps, MatchedPart, ParsedText, Pattern } from './type'; 4 | 5 | type Parsed = Array>; 6 | 7 | export const textExtraction = ( 8 | text: string, 9 | patterns: Array & CustomTextProps>, 10 | ) => { 11 | let parsedTexts: Parsed = [{ children: text ?? '' }]; 12 | patterns.forEach((pattern: any) => { 13 | const newParts: Parsed = []; 14 | 15 | parsedTexts.forEach(parsedText => { 16 | if (parsedText._matched) { 17 | newParts.push(parsedText); 18 | 19 | return; 20 | } 21 | 22 | const parts: Parsed = []; 23 | 24 | let textLeft = parsedText.children as string; 25 | let indexOfMatchedString = 0; 26 | let matches; 27 | pattern.pattern.lastIndex = 0; 28 | 29 | while (textLeft && (matches = pattern.pattern.exec(textLeft))) { 30 | const previousText = textLeft.substring(0, matches.index); 31 | 32 | indexOfMatchedString = matches.index; 33 | 34 | parts.push({ children: previousText }); 35 | 36 | parts.push( 37 | getMatchedPart(pattern, matches[0], matches, indexOfMatchedString), 38 | ); 39 | 40 | textLeft = textLeft.substring(matches.index + matches[0].length); 41 | 42 | indexOfMatchedString += matches[0].length - 1; 43 | 44 | pattern.pattern.lastIndex = 0; 45 | } 46 | 47 | parts.push({ children: textLeft }); 48 | 49 | newParts.push(...parts); 50 | }); 51 | 52 | parsedTexts = newParts; 53 | }); 54 | 55 | parsedTexts.forEach(parsedText => delete parsedText._matched); 56 | 57 | return parsedTexts.filter(t => !!t.children); 58 | }; 59 | 60 | // eslint-disable-next-line max-params 61 | function getMatchedPart( 62 | pattern: Record, 63 | text: string, 64 | _match: Array, 65 | index: number, 66 | ): MatchedPart & { children: string } { 67 | const props: MatchedPart = {} as MatchedPart; 68 | 69 | Object.keys(pattern).forEach((key: string) => { 70 | if (key === 'pattern') { 71 | return; 72 | } 73 | 74 | if (key === 'onPress' || key === 'onLongPress') { 75 | // Support onPress / onLongPress functions 76 | props[key] = () => { 77 | (pattern as any)[key](text, index); 78 | }; 79 | } else { 80 | // Set a prop with an arbitrary name to the value in the match-config 81 | (props as any)[key] = pattern[key]; 82 | } 83 | }); 84 | 85 | let customChildren = text; 86 | if (pattern.renderText && typeof pattern.renderText === 'function') { 87 | customChildren = pattern.renderText(text); 88 | } 89 | 90 | return { 91 | ...props, 92 | _matched: true, 93 | children: customChildren, 94 | }; 95 | } 96 | -------------------------------------------------------------------------------- /template/src/app/library/components/post-delay/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import { Freeze } from 'react-freeze'; 4 | 5 | import { PostDelayProps } from './type'; 6 | 7 | const DURATION = 300; 8 | 9 | export const PostDelay = ({ 10 | children, 11 | durationMs = DURATION, 12 | }: PostDelayProps) => { 13 | // state 14 | const [loaded, setLoaded] = useState(false); 15 | 16 | // effect 17 | useEffect(() => { 18 | const id = setTimeout(() => { 19 | setLoaded(true); 20 | }, durationMs); 21 | 22 | return () => { 23 | clearTimeout(id); 24 | }; 25 | // eslint-disable-next-line react-hooks/exhaustive-deps 26 | }, []); 27 | 28 | // render 29 | return {children}; 30 | }; 31 | -------------------------------------------------------------------------------- /template/src/app/library/components/post-delay/type.ts: -------------------------------------------------------------------------------- 1 | import { PropsWithChildren } from 'react'; 2 | 3 | export type PostDelayProps = { 4 | durationMs?: number; 5 | } & PropsWithChildren; 6 | -------------------------------------------------------------------------------- /template/src/app/library/components/radio-button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TouchableWithoutFeedback } from 'react-native'; 3 | 4 | import { interpolate, useAnimatedStyle } from 'react-native-reanimated'; 5 | import { createStyleSheet, useStyles } from 'react-native-unistyles'; 6 | 7 | import { useSharedTransition } from '@animated'; 8 | import { AnimatedView } from '@rn-core'; 9 | 10 | import { RadioButtonProps } from './type'; 11 | 12 | export const RadioButton = ({ 13 | value, 14 | onToggle, 15 | size = 24, 16 | disabled = false, 17 | initialValue = false, 18 | }: RadioButtonProps) => { 19 | // state 20 | const { styles } = useStyles(stylesSheet); 21 | 22 | const [localValue, setLocalValue] = useState(initialValue); 23 | 24 | const progress = useSharedTransition( 25 | isTypeof(value, 'boolean') ? value : localValue, 26 | { duration: 200 }, 27 | ); 28 | 29 | // function 30 | const onPress = () => { 31 | if (typeof value === 'boolean') { 32 | execFunc(onToggle, !value); 33 | } else { 34 | execFunc(onToggle, !localValue); 35 | 36 | setLocalValue(v => !v); 37 | } 38 | }; 39 | 40 | // style 41 | const dotStyle = useAnimatedStyle(() => ({ 42 | opacity: progress.value, 43 | transform: [{ scale: interpolate(progress.value, [0, 1], [0, 1]) }], 44 | })); 45 | 46 | // render 47 | return ( 48 | 49 | 50 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | const stylesSheet = createStyleSheet(theme => ({ 60 | container: (size: number, disabled?: boolean) => ({ 61 | alignItems: 'center', 62 | backgroundColor: theme.color.neutral50, 63 | borderColor: disabled ? theme.color.neutral200 : theme.color.technical, 64 | borderRadius: size, 65 | borderWidth: 1, 66 | height: size, 67 | justifyContent: 'center', 68 | position: 'relative', 69 | width: size, 70 | }), 71 | dot: (size: number, disabled) => { 72 | return { 73 | alignSelf: 'center', 74 | backgroundColor: disabled 75 | ? theme.color.neutral200 76 | : theme.color.primary500, 77 | borderRadius: size / 4, 78 | height: size / 2, 79 | position: 'absolute', 80 | width: size / 2, 81 | }; 82 | }, 83 | })); 84 | -------------------------------------------------------------------------------- /template/src/app/library/components/radio-button/type.ts: -------------------------------------------------------------------------------- 1 | export interface RadioButtonProps { 2 | /** 3 | * Default state of radio button 4 | * @default false 5 | */ 6 | initialValue?: boolean; 7 | 8 | /** 9 | * Radio button size 10 | * @default 24 11 | */ 12 | size?: number; 13 | 14 | /** 15 | * Overwrite value 16 | * @default undefined 17 | */ 18 | value?: boolean; 19 | 20 | /** 21 | * On radio button press 22 | */ 23 | onToggle?: (value: boolean) => void; 24 | 25 | /** 26 | * Radio button is disabled 27 | * @default false 28 | */ 29 | disabled?: boolean; 30 | } 31 | -------------------------------------------------------------------------------- /template/src/app/library/components/screen/type.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | NativeScrollEvent, 4 | NativeSyntheticEvent, 5 | StyleProp, 6 | ViewStyle, 7 | } from 'react-native'; 8 | 9 | import { Edge } from 'react-native-safe-area-context'; 10 | 11 | import { StatusBarStyle } from 'expo-status-bar'; 12 | 13 | export type ScreenProps = { 14 | /** 15 | * Children of Screen 16 | */ 17 | children?: React.ReactNode; 18 | 19 | /** 20 | * Overwrite style of screen 21 | * @default undefined 22 | */ 23 | style?: StyleProp; 24 | 25 | /** 26 | * Color of Screen 27 | * @default transparent 28 | */ 29 | backgroundColor?: string; 30 | 31 | /** 32 | * Status bar style 33 | * @default dark-content 34 | */ 35 | statusBarStyle?: StatusBarStyle; 36 | 37 | /** 38 | * Using safe area on ios 39 | * @default false 40 | */ 41 | unsafe?: boolean; 42 | 43 | /** 44 | * Visibility status bar 45 | * @default true 46 | */ 47 | hiddenStatusBar?: boolean; 48 | 49 | /** 50 | * Color of status bar for both Android/IOS 51 | */ 52 | statusColor?: string; 53 | 54 | /** 55 | * Color of inset bottom 56 | * @default #ffffff 57 | */ 58 | bottomInsetColor?: string; 59 | 60 | /** 61 | * Color of inset left 62 | * @default #ffffff 63 | */ 64 | leftInsetColor?: string; 65 | 66 | /** 67 | * Color of inset left 68 | * @default #ffffff 69 | */ 70 | rightInsetColor?: string; 71 | 72 | /** 73 | * Using scroll content 74 | * @default false 75 | */ 76 | scroll?: boolean; 77 | 78 | /** 79 | * Inset for safe area view 80 | * @default undefined 81 | */ 82 | excludeEdges?: 'all' | Edge[]; 83 | 84 | /** 85 | * Animated onScroll 86 | * @default undefined 87 | */ 88 | onScroll?: (event: NativeSyntheticEvent) => void; 89 | }; 90 | 91 | export type InsetComponentProps = Pick< 92 | ScreenProps, 93 | | 'statusColor' 94 | | 'unsafe' 95 | | 'hiddenStatusBar' 96 | | 'bottomInsetColor' 97 | | 'leftInsetColor' 98 | | 'rightInsetColor' 99 | | 'statusBarStyle' 100 | > & { 101 | edges: Edge[]; 102 | }; 103 | 104 | export interface InsetProps { 105 | color?: string; 106 | height: number; 107 | width: number; 108 | top?: number; 109 | left?: number; 110 | right?: number; 111 | bottom?: number; 112 | } 113 | 114 | export type ScreenComponentProps = ReOmit< 115 | ScreenProps, 116 | 'unsafe' | 'scroll' | 'excludeEdges' 117 | > & { 118 | edges: Edge[]; 119 | actualUnsafe: boolean; 120 | }; 121 | -------------------------------------------------------------------------------- /template/src/app/library/components/snack-bar/constants.ts: -------------------------------------------------------------------------------- 1 | const DURATION_HIDE = 1000; 2 | 3 | const DURATION_ANIMATED = 500; 4 | 5 | const BG_SUCCESS = '#00875A'; 6 | 7 | const BG_LINK = '#0052CC'; 8 | 9 | const BG_ERROR = '#DE350B'; 10 | 11 | const BG_WARN = '#FFC400'; 12 | 13 | export { 14 | DURATION_HIDE, 15 | DURATION_ANIMATED, 16 | BG_SUCCESS, 17 | BG_LINK, 18 | BG_ERROR, 19 | BG_WARN, 20 | }; 21 | -------------------------------------------------------------------------------- /template/src/app/library/components/snack-bar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createRef, 3 | forwardRef, 4 | useCallback, 5 | useImperativeHandle, 6 | useState, 7 | } from 'react'; 8 | import { StyleSheet } from 'react-native'; 9 | 10 | import { View } from '@rn-core'; 11 | 12 | import { DURATION_HIDE } from './constants'; 13 | import { SnackItem } from './snack-bar-item'; 14 | import { styles } from './styles'; 15 | import { Item, TypeMessage } from './type'; 16 | 17 | const SnackBarComponent = forwardRef((_, ref) => { 18 | // state 19 | const [data, setData] = useState([]); 20 | 21 | // function 22 | const onPop = useCallback((item: Item) => { 23 | setData(d => d.filter(x => x.id !== item.id)); 24 | }, []); 25 | 26 | const _renderItem = (item: Item, index: number) => ( 27 | 28 | ); 29 | 30 | // effect 31 | useImperativeHandle( 32 | ref, 33 | () => ({ 34 | show: ({ 35 | interval = DURATION_HIDE, 36 | msg, 37 | type = 'success', 38 | }: { 39 | msg: string; 40 | interval: number; 41 | type: TypeMessage; 42 | }) => { 43 | setData(d => 44 | d.concat([ 45 | { 46 | id: randomUniqueId(), 47 | interval, 48 | msg, 49 | type, 50 | }, 51 | ]), 52 | ); 53 | }, 54 | }), 55 | [], 56 | ); 57 | 58 | // render 59 | return ( 60 | 63 | {data.map(_renderItem)} 64 | 65 | ); 66 | }); 67 | 68 | type SnackBar = { 69 | show: (data: { msg: string; interval?: number; type?: TypeMessage }) => void; 70 | }; 71 | 72 | export const snackBarRef = createRef(); 73 | 74 | export const SnackBar = () => ; 75 | 76 | export const showSnack = ({ 77 | msg, 78 | interval, 79 | type, 80 | }: { 81 | msg: string; 82 | interval?: number; 83 | type?: TypeMessage; 84 | }) => { 85 | snackBarRef.current?.show({ interval, msg, type }); 86 | }; 87 | -------------------------------------------------------------------------------- /template/src/app/library/components/snack-bar/styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | minHeight: 50, 6 | }, 7 | itemBar: { 8 | // alignSelf: 'center', 9 | alignItems: 'center', 10 | 11 | flexDirection: 'row', 12 | 13 | paddingHorizontal: 15, 14 | 15 | paddingVertical: 13, 16 | 17 | position: 'absolute', 18 | width: '100%', 19 | }, 20 | text: { 21 | flex: 1, 22 | marginTop: -2, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /template/src/app/library/components/snack-bar/type.ts: -------------------------------------------------------------------------------- 1 | export const TYPE_MESSAGE = { 2 | ERROR: 'error', 3 | LINK: 'link', 4 | SUCCESS: 'success', 5 | WARN: 'warn', 6 | } as const; 7 | 8 | export type TypeMessage = (typeof TYPE_MESSAGE)[keyof typeof TYPE_MESSAGE]; 9 | 10 | export type Item = { 11 | id: string; 12 | msg: string; 13 | type: TypeMessage; 14 | interval: number; 15 | }; 16 | 17 | export interface SnackBarItemProps { 18 | item: Item; 19 | index: number; 20 | onPop: (item: Item) => void; 21 | } 22 | 23 | export type DataShowMessage = { 24 | msg: string; 25 | type: TypeMessage; 26 | interval?: number; 27 | }; 28 | -------------------------------------------------------------------------------- /template/src/app/library/components/spacer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { StyleProp, ViewStyle } from 'react-native'; 3 | 4 | import { View } from '@components/core'; 5 | 6 | import { SpacerProps } from './type'; 7 | 8 | export const Spacer = ({ height = 0, width = 0 }: SpacerProps) => { 9 | // style 10 | const actualStyle = useMemo>( 11 | () => ({ 12 | height, 13 | width, 14 | }), 15 | [height, width], 16 | ); 17 | 18 | // render 19 | return ; 20 | }; 21 | -------------------------------------------------------------------------------- /template/src/app/library/components/spacer/type.ts: -------------------------------------------------------------------------------- 1 | import { DimensionValue } from 'react-native'; 2 | 3 | export interface SpacerProps { 4 | /** 5 | * Width of size box 6 | * @default 0 7 | */ 8 | width?: DimensionValue; 9 | 10 | /** 11 | * Height of size box 12 | * @default 0 13 | */ 14 | height?: DimensionValue; 15 | } 16 | -------------------------------------------------------------------------------- /template/src/app/library/components/stack-view/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import React, { forwardRef } from 'react'; 3 | 4 | import Animated from 'react-native-reanimated'; 5 | 6 | import { StackViewProps } from './type'; 7 | 8 | export const StackView = forwardRef( 9 | ({ children, ...rest }: StackViewProps, ref: any) => { 10 | // render 11 | return ( 12 | 18 | {children} 19 | 20 | ); 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /template/src/app/library/components/stack-view/type.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ScrollViewProps } from 'react-native'; 3 | 4 | import { AnimatedProps } from 'react-native-reanimated'; 5 | 6 | export interface StackViewProps extends AnimatedProps { 7 | children?: React.ReactNode; 8 | } 9 | -------------------------------------------------------------------------------- /template/src/app/library/components/tabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useSharedValue } from 'react-native-reanimated'; 4 | import { createStyleSheet, useStyles } from 'react-native-unistyles'; 5 | 6 | import { View } from '@rn-core'; 7 | 8 | import { TabItem } from './tab-item'; 9 | import { Tab, TabsProps } from './type'; 10 | 11 | export const Tabs = ({ tabs, initialIndex = 0 }: TabsProps) => { 12 | // state 13 | const { styles } = useStyles(styleSheet); 14 | 15 | const selectedIndex = useSharedValue(initialIndex); 16 | 17 | // func 18 | const renderTab = (item: Tab, index: number) => { 19 | return ( 20 | 26 | ); 27 | }; 28 | 29 | // render 30 | return {tabs.map(renderTab)}; 31 | }; 32 | 33 | const styleSheet = createStyleSheet(theme => ({ 34 | container: { 35 | backgroundColor: theme.color.neutral50, 36 | flexDirection: 'row', 37 | }, 38 | })); 39 | -------------------------------------------------------------------------------- /template/src/app/library/components/tabs/tab-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useTranslation } from 'react-i18next'; 4 | import { useAnimatedStyle } from 'react-native-reanimated'; 5 | import { createStyleSheet, useStyles } from 'react-native-unistyles'; 6 | 7 | import { DefaultButton } from '@components/button/default-button'; 8 | import { AnimatedText, AnimatedView, View } from '@rn-core'; 9 | 10 | import { TabItemProps } from './type'; 11 | 12 | export const TabItem = ({ tab, index, selectedIndex }: TabItemProps) => { 13 | // state 14 | const { styles, theme } = useStyles(styleSheet); 15 | 16 | const [t] = useTranslation(); 17 | 18 | // func 19 | const handlePress = () => { 20 | if (selectedIndex.value === index) { 21 | return; 22 | } 23 | 24 | selectedIndex.value = index; 25 | }; 26 | 27 | // style 28 | const underlineStyle = useAnimatedStyle(() => ({ 29 | opacity: selectedIndex.value === index ? 1 : 0, 30 | })); 31 | 32 | const textStyle = useAnimatedStyle(() => ({ 33 | color: 34 | selectedIndex.value === index 35 | ? theme.color.neutral500 36 | : theme.color.neutral300, 37 | })); 38 | 39 | // render 40 | return ( 41 | 42 | 43 | 44 | 45 | {t(tab.title)} 46 | 47 | 48 | 49 | 50 | 54 | 55 | ); 56 | }; 57 | 58 | const styleSheet = createStyleSheet(theme => ({ 59 | button: { 60 | alignItems: 'center', 61 | justifyContent: 'center', 62 | paddingHorizontal: 8, 63 | paddingVertical: 12, 64 | }, 65 | container: { 66 | flex: 1, 67 | }, 68 | underline: { 69 | backgroundColor: theme.color.primary500, 70 | bottom: 0, 71 | height: 2, 72 | left: 0, 73 | position: 'absolute', 74 | right: 0, 75 | zIndex: 99, 76 | }, 77 | underlineOverlay: { 78 | backgroundColor: theme.color.primary50, 79 | bottom: 0, 80 | height: 2, 81 | left: 0, 82 | position: 'absolute', 83 | right: 0, 84 | zIndex: 9, 85 | }, 86 | })); 87 | -------------------------------------------------------------------------------- /template/src/app/library/components/tabs/type.ts: -------------------------------------------------------------------------------- 1 | import { SharedValue } from 'react-native-reanimated'; 2 | 3 | export type Tab = { 4 | title: I18nKeys; 5 | key: string; 6 | }; 7 | 8 | export type TabsProps = { 9 | /** 10 | * Start index 11 | * @default 0 12 | */ 13 | initialIndex?: number; 14 | /** 15 | * data for tabs 16 | */ 17 | tabs: Array; 18 | }; 19 | 20 | export type TabItemProps = { 21 | tab: Tab; 22 | index: number; 23 | selectedIndex: SharedValue; 24 | }; 25 | -------------------------------------------------------------------------------- /template/src/app/library/networking/api.ts: -------------------------------------------------------------------------------- 1 | const API_VERSION = '/api/v1/'; 2 | 3 | const ApiEndPoint = { 4 | LOGIN: '', 5 | REFRESH_TOKEN: '', 6 | } as const; 7 | 8 | const configApi = () => { 9 | const apiOb: Record = {}; 10 | 11 | Object.keys(ApiEndPoint).forEach(x => { 12 | const valueApi = ApiEndPoint[x as keyof typeof ApiEndPoint]; 13 | 14 | apiOb[x] = API_VERSION + valueApi; 15 | }); 16 | 17 | return apiOb; 18 | }; 19 | 20 | type ApiConstantsType = { 21 | [a in keyof T]: string; 22 | }; 23 | 24 | export const ApiConstants = configApi() as ApiConstantsType; 25 | -------------------------------------------------------------------------------- /template/src/app/library/utils/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { initReactI18next } from 'react-i18next'; 2 | 3 | import i18n, { ParseKeys, TOptions, TypeOptions } from 'i18next'; 4 | 5 | const defaultNS = 'en'; 6 | 7 | import en from './source/en.json'; 8 | 9 | const resources = { en } as const; 10 | 11 | declare module 'i18next' { 12 | interface CustomTypeOptions { 13 | defaultNS: typeof defaultNS; 14 | resources: typeof resources; 15 | keySeparator: ':'; 16 | } 17 | } 18 | 19 | declare global { 20 | type I18nKeys = ParseKeys; 21 | } 22 | 23 | (() => { 24 | i18n.use(initReactI18next).init({ 25 | fallbackLng: defaultNS, 26 | interpolation: { 27 | escapeValue: false, 28 | }, 29 | keySeparator: false, 30 | lng: defaultNS, 31 | resources, 32 | }); 33 | })(); 34 | 35 | export default i18n; 36 | -------------------------------------------------------------------------------- /template/src/app/library/utils/i18n/source/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": { 3 | "-100": "Connection errors! Please check the connection again.", 4 | "0": "Request Timeout", 5 | "400": "Bad Request", 6 | "401": "Unauthorized", 7 | "402": "Payment Required ", 8 | "403": "Forbidden", 9 | "404": "Not Found", 10 | "405": "Method Not Allowed", 11 | "406": "Not Acceptable", 12 | "407": "Proxy Authentication Required", 13 | "408": "Request Timeout", 14 | "409": "Conflict", 15 | "410": "Gone", 16 | "411": "Length Required", 17 | "412": "Precondition Failed", 18 | "413": "Payload Too Large", 19 | "414": "URI Too Long", 20 | "415": "Unsupported Media Type", 21 | "416": "Range Not Satisfiable", 22 | "417": "Expectation Failed", 23 | "500": "Internal Server Error", 24 | "501": "Not Implemented", 25 | "502": "Bad Gateway", 26 | "503": "Service Unavailable", 27 | "504": "Gateway Timeout", 28 | "505": "HTTP Version Not Supported", 29 | "have_error": "An error occurred. Please try again later", 30 | "error_on_request": "An error occurred while sending the request", 31 | "error_on_handle": "An error occurred while processing data", 32 | "server_error": "Server Error" 33 | }, 34 | 35 | "date": { 36 | "just_now": "Just now", 37 | "minute_ago_one": "{{count}} minute ago", 38 | "minute_ago_other": "{{count}} minutes ago", 39 | "hour_ago_one": "{{count}} hour ago", 40 | "hour_ago_other": "{{count}} hours ago", 41 | "yesterday": "Yesterday", 42 | "days_ago": "{{count}} days ago", 43 | "last_week": "Last week", 44 | "last_month": "Last month", 45 | "months_ago": "{{count}} months ago", 46 | "last_year": "Last year", 47 | "years_ago": "{{count}} years ago" 48 | }, 49 | "dialog": { 50 | "error": "Error", 51 | "cancel": "Cancel", 52 | "warning": "Warn", 53 | "loading": "Loading", 54 | "select": "Select" 55 | }, 56 | "validation": { 57 | "email_required": "Email is required" 58 | }, 59 | "tabs": { 60 | "tab1": "Tab 1", 61 | "tab2": "Tab 2", 62 | "tab3": "Tab 3", 63 | "tab4": "Tab 4" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /template/src/app/library/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { MMKV } from 'react-native-mmkv'; 3 | 4 | import { APP_DISPLAY_NAME, PRIVATE_KEY_STORAGE } from '@env'; 5 | import { StateStorage } from 'zustand/middleware'; 6 | 7 | export const AppStorage = new MMKV({ 8 | encryptionKey: PRIVATE_KEY_STORAGE, 9 | id: `user-${APP_DISPLAY_NAME}-storage`, 10 | }); 11 | 12 | /** 13 | * Loads a string from storage. 14 | * 15 | * @param key The key to fetch. 16 | */ 17 | export function loadString(key: string) { 18 | try { 19 | return AppStorage.getString(key); 20 | } catch { 21 | // not sure why this would fail... even reading the RN docs I'm unclear 22 | return undefined; 23 | } 24 | } 25 | 26 | /** 27 | * Saves a string to storage. 28 | * 29 | * @param key The key to fetch. 30 | * @param value The value to store. 31 | */ 32 | export function saveString(key: string, value: string) { 33 | try { 34 | AppStorage.set(key, value); 35 | 36 | return true; 37 | } catch { 38 | return false; 39 | } 40 | } 41 | 42 | /** 43 | * Loads something from storage and runs it thru JSON.parse. 44 | * 45 | * @param key The key to fetch. 46 | */ 47 | export function load>(key: string): T | null { 48 | try { 49 | const almostThere = AppStorage.getString(key); 50 | 51 | return typeof almostThere === 'string' ? JSON.parse(almostThere) : null; 52 | } catch { 53 | return null; 54 | } 55 | } 56 | 57 | /** 58 | * Saves an object to storage. 59 | * 60 | * @param key The key to fetch. 61 | * @param value The value to store. 62 | */ 63 | export function save(key: string, value: any) { 64 | try { 65 | AppStorage.set(key, JSON.stringify(value)); 66 | 67 | return true; 68 | } catch { 69 | return false; 70 | } 71 | } 72 | 73 | /** 74 | * Removes something from storage. 75 | * 76 | * @param key The key to kill. 77 | */ 78 | export async function remove(key: string) { 79 | try { 80 | AppStorage.delete(key); 81 | } catch {} 82 | } 83 | 84 | export const zustandStorage: StateStorage = { 85 | getItem: name => { 86 | const value = AppStorage.getString(name); 87 | 88 | return value ?? null; 89 | }, 90 | removeItem: name => { 91 | return AppStorage.delete(name); 92 | }, 93 | setItem: (name, value) => { 94 | return AppStorage.set(name, value); 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /template/src/app/model/authentication.ts: -------------------------------------------------------------------------------- 1 | export type FormLoginType = { 2 | email: string; 3 | password: string; 4 | }; 5 | -------------------------------------------------------------------------------- /template/src/app/model/navigation-params.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /template/src/app/navigation/app-container.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { SnackBar } from '@components/snack-bar'; 4 | import { PortalHost } from '@gorhom/portal'; 5 | import { RootNavigation } from '@navigation/root-navigator'; 6 | import { selectAppLoading } from '@selectors/app'; 7 | import { appServices } from '@services/app'; 8 | import { useAppStore } from '@stores/app'; 9 | import { useShallow } from 'zustand/shallow'; 10 | 11 | export const AppContainer = () => { 12 | // state 13 | const { loadingApp } = useAppStore(useShallow(selectAppLoading)); 14 | 15 | // effect 16 | useEffect(() => { 17 | appServices.startLoadApp(); 18 | }, []); 19 | 20 | // render 21 | return ( 22 | <> 23 | {!loadingApp && ( 24 | <> 25 | 26 | 27 | 28 | 29 | )} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /template/src/app/navigation/navigation-service.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CommonActions, 3 | createNavigationContainerRef, 4 | StackActions, 5 | } from '@react-navigation/native'; 6 | 7 | import { RootStackParamList } from './screen-types'; 8 | 9 | export const navigationRef = createNavigationContainerRef(); 10 | 11 | export function navigateScreen( 12 | ...arg: undefined extends RootStackParamList[RouteName] 13 | ? 14 | | [screen: RouteName] 15 | | [screen: RouteName, params?: RootStackParamList[RouteName]] 16 | : [screen: RouteName, params: RootStackParamList[RouteName]] 17 | ) { 18 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 19 | //@ts-ignore 20 | navigationRef.navigate(arg[0], arg.length > 1 ? arg[1] : undefined); 21 | } 22 | 23 | export function pushScreen( 24 | ...arg: undefined extends RootStackParamList[RouteName] 25 | ? 26 | | [screen: RouteName] 27 | | [screen: RouteName, params?: RootStackParamList[RouteName]] 28 | : [screen: RouteName, params: RootStackParamList[RouteName]] 29 | ) { 30 | navigationRef.dispatch( 31 | StackActions.push(arg[0], arg.length > 1 ? arg[1] : undefined), 32 | ); 33 | } 34 | 35 | export function replaceScreen( 36 | ...arg: undefined extends RootStackParamList[RouteName] 37 | ? 38 | | [screen: RouteName] 39 | | [screen: RouteName, params?: RootStackParamList[RouteName]] 40 | : [screen: RouteName, params: RootStackParamList[RouteName]] 41 | ) { 42 | navigationRef.dispatch( 43 | StackActions.replace(arg[0], arg.length > 1 ? arg[1] : undefined), 44 | ); 45 | } 46 | 47 | export function goBack() { 48 | navigationRef.dispatch(CommonActions.goBack); 49 | } 50 | -------------------------------------------------------------------------------- /template/src/app/navigation/root-navigator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import BootSplash from 'react-native-bootsplash'; 4 | import { useStyles } from 'react-native-unistyles'; 5 | 6 | import { APP_SCREEN } from '@navigation/screen-types'; 7 | import { createStaticNavigation, DefaultTheme } from '@react-navigation/native'; 8 | import { createNativeStackNavigator } from '@react-navigation/native-stack'; 9 | import { Home } from '@screens/authentication/home'; 10 | import { Login } from '@screens/un-authentication/login'; 11 | import { selectAppToken } from '@selectors/app'; 12 | import { useAppStore } from '@stores/app'; 13 | 14 | import { navigationRef } from './navigation-service'; 15 | 16 | const useAppLoggedIn = () => { 17 | return useAppStore(selectAppToken) !== undefined; 18 | }; 19 | 20 | const useAppLoggedOut = () => { 21 | return !useAppLoggedIn(); 22 | }; 23 | 24 | const RootStack = createNativeStackNavigator({ 25 | groups: { 26 | [APP_SCREEN.UN_AUTHORIZE]: { 27 | if: useAppLoggedIn, 28 | screens: { 29 | [APP_SCREEN.HOME]: Home, 30 | }, 31 | }, 32 | [APP_SCREEN.AUTHORIZE]: { 33 | if: useAppLoggedOut, 34 | screenOptions: { 35 | headerShown: false, 36 | }, 37 | screens: { 38 | [APP_SCREEN.LOGIN]: { 39 | screen: Login, 40 | }, 41 | }, 42 | }, 43 | }, 44 | screenOptions: { 45 | freezeOnBlur: true, 46 | }, 47 | }); 48 | 49 | const Navigation = createStaticNavigation(RootStack); 50 | 51 | export const RootNavigation = () => { 52 | // state 53 | 54 | const { theme } = useStyles(); 55 | 56 | // effect 57 | useEffect(() => { 58 | const id = setTimeout(() => { 59 | BootSplash.hide({ fade: true }); 60 | }, 1000); 61 | 62 | return () => clearTimeout(id); 63 | }, []); 64 | 65 | // render 66 | return ( 67 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /template/src/app/navigation/screen-types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | import { NativeStackScreenProps } from '@react-navigation/native-stack'; 3 | 4 | export enum APP_SCREEN { 5 | UN_AUTHORIZE = 'UN_AUTHORIZE', 6 | LOGIN = 'LOGIN', 7 | 8 | AUTHORIZE = 'AUTHORIZE', 9 | HOME = 'HOME', 10 | } 11 | 12 | export type RootStackParamList = { 13 | [APP_SCREEN.LOGIN]: undefined; 14 | [APP_SCREEN.UN_AUTHORIZE]: undefined; 15 | [APP_SCREEN.AUTHORIZE]: undefined; 16 | [APP_SCREEN.HOME]: undefined; 17 | }; 18 | 19 | export type StackScreenProps = 20 | NativeStackScreenProps; 21 | 22 | declare global { 23 | namespace ReactNavigation { 24 | interface RootParamList extends RootStackParamList {} 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /template/src/app/screens/authentication/home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Text, View } from '@rn-core'; 4 | 5 | export const Home = () => { 6 | // render 7 | return ( 8 | 9 | Home 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /template/src/app/screens/un-authentication/login/type.ts: -------------------------------------------------------------------------------- 1 | import { FormLoginType } from '@model/authentication'; 2 | 3 | export interface FormLoginProps { 4 | onSubmit: (data: FormLoginType) => void; 5 | } 6 | -------------------------------------------------------------------------------- /template/src/app/services/app.ts: -------------------------------------------------------------------------------- 1 | import { MMKV_KEY } from '@common/constant'; 2 | import { useAppStore } from '@stores/app'; 3 | import { loadString } from '@utils/storage'; 4 | 5 | const startLoadApp = async () => { 6 | useAppStore.getState().startLoadApp(); 7 | 8 | const token = loadString(MMKV_KEY.APP_TOKEN); 9 | 10 | if (typeof token === 'string') { 11 | useAppStore.getState().setToken(token); 12 | } 13 | 14 | useAppStore.getState().endLoadApp(); 15 | }; 16 | 17 | export const appServices = { 18 | startLoadApp, 19 | }; 20 | -------------------------------------------------------------------------------- /template/src/app/services/authentication.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { createSignal, withLoadingFunc } from '@common/signal'; 3 | import { ApiConstants } from '@networking/api'; 4 | import { validResponse } from '@networking/helper'; 5 | import { NetWorkService } from '@networking/service'; 6 | 7 | const signalObj = createSignal(['login'] as const); 8 | 9 | const login = async (body: any) => { 10 | await withLoadingFunc(async () => { 11 | const response = await NetWorkService.Post({ 12 | body, 13 | signal: signalObj.login.signal, 14 | url: ApiConstants.LOGIN, 15 | }); 16 | 17 | if (!response) { 18 | return; 19 | } 20 | 21 | if (validResponse(response)) { 22 | /** 23 | * Do something when login success 24 | */ 25 | } 26 | }); 27 | }; 28 | 29 | export const authenticationService = { 30 | login, 31 | }; 32 | -------------------------------------------------------------------------------- /template/src/app/themes/colors/dark.ts: -------------------------------------------------------------------------------- 1 | export const darkColors = { 2 | background: '#141417', 3 | 4 | danger: '#E96F6F', 5 | danger100: '#4F2729', 6 | danger20: '#2F1A1D', 7 | danger200: '#773335', 8 | danger300: '#8B3A3B', 9 | danger400: '#B24647', 10 | danger50: '#321D20', 11 | 12 | danger500: '#DA5353', 13 | 14 | info: '#87C2F0', 15 | info100: '#2E4457', 16 | info200: '#3F6482', 17 | info300: '#487497', 18 | info400: '#5994C2', 19 | info50: '#1D242C', 20 | info500: '#6BB4ED', 21 | 22 | neutral: '#EAE9F2', 23 | 24 | neutral100: '#42404D', 25 | neutral200: '#615F6D', 26 | neutral300: '#7A7887', 27 | neutral400: '#A4A2B4', 28 | neutral50: '#201F26', 29 | neutral500: '#E0DEEA', 30 | primary: '#F193B2', 31 | 32 | primary100: '#52313E', 33 | primary200: '#7B4558', 34 | primary300: '#904E65', 35 | primary400: '#BA627F', 36 | primary50: '#3D2731', 37 | primary500: '#E37599', 38 | primaryGradient: ['#E37599', '#F2CB78'], 39 | 40 | secondary: '#FEE2A5', 41 | secondary100: '#574B34', 42 | secondary200: '#836F47', 43 | secondary300: '#998251', 44 | secondary400: '#C6A665', 45 | secondary50: '#332E29', 46 | secondary500: '#F2CB78', 47 | 48 | success: '#91F2BB', 49 | success100: '#2E5240', 50 | success200: '#407C5B', 51 | success300: '#499169', 52 | success400: '#5ABA85', 53 | success50: '#21332C', 54 | success500: '#6CE4A0', 55 | technical: '#7E7F96', 56 | 57 | warning: '#F4D168', 58 | warning100: '#564925', 59 | warning200: '#826D2E', 60 | warning300: '#987E32', 61 | warning400: '#C4A23B', 62 | warning50: '#352F1E', 63 | warning500: '#F0C544', 64 | } as const; 65 | -------------------------------------------------------------------------------- /template/src/app/themes/colors/light.ts: -------------------------------------------------------------------------------- 1 | export const lightColors = { 2 | background: '#F2F8FC', 3 | 4 | danger: '#B41313', 5 | danger100: '#EFA8A8', 6 | danger20: '#FCF2F2', 7 | danger200: '#EB8C8C', 8 | danger300: '#E26060', 9 | danger400: '#D94949', 10 | danger50: '#F9E3E3', 11 | 12 | danger500: '#C92B2B', 13 | 14 | info: '#248DDE', 15 | info100: '#BCE0FB', 16 | info200: '#9BCFF6', 17 | info300: '#7ABEF2', 18 | info400: '#59ADEE', 19 | info50: '#DEF1FF', 20 | info500: '#3D9EE9', 21 | 22 | neutral: '#141417', 23 | 24 | neutral100: '#E6E5EA', 25 | neutral200: '#C7C5CD', 26 | neutral300: '#83818E', 27 | neutral400: '#4F4E59', 28 | neutral50: '#F4F3F5', 29 | neutral500: '#2B2A31', 30 | primary: '#BC305D', 31 | 32 | primary100: '#F5C0D2', 33 | primary200: '#F2A3BD', 34 | primary300: '#E27A9C', 35 | primary400: '#D46287', 36 | primary50: '#FDE1EB', 37 | primary500: '#C84771', 38 | primaryGradient: ['#C84771', '#ECBC55'], 39 | 40 | secondary: '#D8A12D', 41 | secondary100: '#F6E7C6', 42 | secondary200: '#F0D9A6', 43 | secondary300: '#ECCF91', 44 | secondary400: '#E5C173', 45 | secondary50: '#F5EFE1', 46 | secondary500: '#ECBC55', 47 | 48 | success: '#26B765', 49 | success100: '#B4EFCD', 50 | success200: '#97E7B9', 51 | success300: '#75DEA2', 52 | success400: '#5ED792', 53 | success50: '#D9F6E5', 54 | success500: '#41C97C', 55 | technical: '#535474', 56 | 57 | warning: '#CF9C00', 58 | warning100: '#FCE399', 59 | warning200: '#F6D369', 60 | warning300: '#F2C746', 61 | warning400: '#EBBA28', 62 | warning50: '#FFF4D2', 63 | warning500: '#E5AF0C', 64 | } as const; 65 | -------------------------------------------------------------------------------- /template/src/app/themes/index.ts: -------------------------------------------------------------------------------- 1 | import { UnistylesRegistry, UnistylesThemes } from 'react-native-unistyles'; 2 | 3 | import { darkColors } from './colors/dark'; 4 | import { lightColors } from './colors/light'; 5 | import { textPresets } from './text-presets'; 6 | 7 | export type Colors = keyof UnistylesThemes['dark']['color']; 8 | const darkTheme = { 9 | color: darkColors, 10 | textPresets: textPresets, 11 | type: 'dark', 12 | }; 13 | 14 | const lightTheme = { 15 | color: lightColors, 16 | textPresets: textPresets, 17 | type: 'light', 18 | }; 19 | 20 | type AppThemes = { 21 | light: typeof lightTheme; 22 | dark: typeof darkTheme; 23 | }; 24 | 25 | UnistylesRegistry.addThemes({ 26 | dark: darkTheme, 27 | light: lightTheme, 28 | }).addConfig({ 29 | adaptiveThemes: true, 30 | // initialTheme: 'light', 31 | }); 32 | 33 | declare module 'react-native-unistyles' { 34 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 35 | export interface UnistylesThemes extends AppThemes {} 36 | } 37 | -------------------------------------------------------------------------------- /template/src/app/themes/text-presets/index.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | import { FontDefault } from '../typography'; 4 | 5 | const presets = { 6 | CTALinks: { 7 | color: '#000000', 8 | fontFamily: FontDefault.primary, 9 | fontSize: 18, 10 | }, 11 | CTASmall: { 12 | color: '#000000', 13 | fontFamily: FontDefault.primary, 14 | fontSize: 14, 15 | }, 16 | CTAs: { 17 | color: '#000000', 18 | fontFamily: FontDefault.primary, 19 | fontSize: 16, 20 | }, 21 | H1: { 22 | color: '#000000', 23 | fontFamily: FontDefault.primarySemiBold, 24 | fontSize: 48, 25 | }, 26 | H2: { 27 | color: '#000000', 28 | fontFamily: FontDefault.primarySemiBold, 29 | fontSize: 40, 30 | }, 31 | H3: { 32 | color: '#000000', 33 | fontFamily: FontDefault.primarySemiBold, 34 | fontSize: 36, 35 | }, 36 | H4: { 37 | color: '#000000', 38 | fontFamily: FontDefault.primarySemiBold, 39 | fontSize: 30, 40 | }, 41 | H5: { 42 | color: '#000000', 43 | fontFamily: FontDefault.primarySemiBold, 44 | fontSize: 24, 45 | }, 46 | assistive: { 47 | color: '#000000', 48 | fontFamily: FontDefault.primary, 49 | fontSize: 1248, 50 | }, 51 | caption: { 52 | color: '#000000', 53 | fontFamily: FontDefault.primary, 54 | fontSize: 12, 55 | }, 56 | extraSmall: { 57 | color: '#000000', 58 | fontFamily: FontDefault.primary, 59 | fontSize: 12, 60 | }, 61 | label: { 62 | color: '#000000', 63 | fontFamily: FontDefault.primary, 64 | fontSize: 12, 65 | }, 66 | overline: { 67 | color: '#000000', 68 | fontFamily: FontDefault.primary, 69 | fontSize: 10, 70 | }, 71 | paragraph1: { 72 | color: '#000000', 73 | fontFamily: FontDefault.secondary, 74 | fontSize: 16, 75 | }, 76 | paragraph2: { 77 | color: '#000000', 78 | fontFamily: FontDefault.primary, 79 | fontSize: 14, 80 | }, 81 | paragraphBold: { 82 | color: '#000000', 83 | fontFamily: FontDefault.primaryBold, 84 | fontSize: 14, 85 | }, 86 | placeholder: { 87 | color: '#000000', 88 | fontFamily: FontDefault.primary, 89 | fontSize: 14, 90 | }, 91 | quotes: { 92 | color: '#000000', 93 | fontFamily: FontDefault.secondaryItalic, 94 | fontSize: 18, 95 | }, 96 | subtitle1: { 97 | color: '#000000', 98 | fontFamily: FontDefault.primarySemiBold, 99 | fontSize: 20, 100 | }, 101 | subtitle2: { 102 | color: '#000000', 103 | fontFamily: FontDefault.primarySemiBold, 104 | fontSize: 18, 105 | }, 106 | }; 107 | 108 | export const textPresets = StyleSheet.create(presets); 109 | -------------------------------------------------------------------------------- /template/src/app/themes/typography/index.ts: -------------------------------------------------------------------------------- 1 | import { fonts } from '@assets/fonts'; 2 | import { useFonts } from 'expo-font'; 3 | 4 | export const FontDefault = { 5 | primary: 'primary', 6 | primaryBold: 'primaryBold', 7 | primarySemiBold: 'primarySemiBold', 8 | secondary: 'secondary', 9 | secondaryItalic: 'secondaryItalic', 10 | } as const; 11 | 12 | export const useLoadFont = () => { 13 | // state 14 | const [isLoaded] = useFonts({ 15 | // icons is default font for react native vector icons. flowing IcMoon to use icons 16 | icons: fonts.icons, 17 | [FontDefault.primary]: fonts.manrope_medium, 18 | [FontDefault.primaryBold]: fonts.manrope_bold, 19 | [FontDefault.primarySemiBold]: fonts.manrope_semibold, 20 | [FontDefault.secondary]: fonts.roboto_regular, 21 | [FontDefault.secondaryItalic]: fonts.roboto_italic, 22 | }); 23 | 24 | return isLoaded; 25 | }; 26 | -------------------------------------------------------------------------------- /template/src/app/zustand/selectors/app.ts: -------------------------------------------------------------------------------- 1 | import { AppState } from '@stores/app'; 2 | 3 | export const selectAppLoading = (state: AppState) => ({ 4 | loadingApp: state.loadingApp, 5 | }); 6 | 7 | export const selectAppToken = (state: AppState) => state.token; 8 | -------------------------------------------------------------------------------- /template/src/app/zustand/stores/app.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { ExtractState } from 'zustand'; 3 | import { immer } from 'zustand/middleware/immer'; 4 | import { create } from 'zustand/react'; 5 | 6 | type State = { 7 | profile: any; 8 | 9 | token: string | undefined; 10 | 11 | loadingApp: boolean; 12 | 13 | loading: boolean; 14 | }; 15 | 16 | type Actions = { 17 | logout: () => void; 18 | endLoadApp: () => void; 19 | setProfile: (payload: State['profile']) => void; 20 | setToken: (token: State['token']) => void; 21 | startLoadApp: () => void; 22 | setLoading: (newState: State['loading']) => void; 23 | }; 24 | 25 | const initialState: State = { 26 | loading: false, 27 | loadingApp: true, 28 | profile: {}, 29 | token: undefined, 30 | }; 31 | 32 | const store = create()( 33 | immer(set => ({ 34 | ...initialState, 35 | endLoadApp: () => { 36 | set(state => { 37 | state.loadingApp = false; 38 | }); 39 | }, 40 | logout: () => { 41 | set(state => { 42 | state.token = undefined; 43 | 44 | state.profile = {}; 45 | }); 46 | }, 47 | setLoading: newState => { 48 | set(state => { 49 | state.loading = newState; 50 | }); 51 | }, 52 | setProfile: payload => { 53 | set(state => { 54 | state.profile = payload; 55 | }); 56 | }, 57 | setToken: token => { 58 | set(state => { 59 | state.token = token; 60 | }); 61 | }, 62 | startLoadApp: () => { 63 | set(state => { 64 | state.loadingApp = true; 65 | }); 66 | }, 67 | })), 68 | ); 69 | 70 | export const useAppStore = store; 71 | 72 | export type AppState = ExtractState; 73 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@env": ["./env-config.ts"], 7 | "@assets/*": ["./assets/*"], 8 | "@common/*": ["./src/app/common/*"], 9 | "@app-emitter": ["./src/app/common/emitter/index"], 10 | "@app-firebase": ["./src/app/common/firebase/index"], 11 | "@hooks": ["./src/app/common/hooks/index"], 12 | "@validate/*": ["./src/app/common/zod-validate/*"], 13 | "@animated": ["./src/app/common/animated/index"], 14 | "@screens/*": ["src/app/screens/*"], 15 | "@rn-core": ["src/app/library/components/core/index"], 16 | "@components/*": ["./src/app/library/components/*"], 17 | "@utils/*": ["./src/app/library/utils/*"], 18 | "@storage": ["./src/app/library/utils/storage/index"], 19 | "@networking/*": ["./src/app/library/networking/*"], 20 | "@model/*": ["./src/app/model/*"], 21 | "@navigation/*": ["./src/app/navigation/*"], 22 | "@theme/*": ["./src/app/themes/*"], 23 | "@stores/*": ["./src/app/zustand/stores/*"], 24 | "@selectors/*": ["./src/app/zustand/selectors/*"], 25 | "@services/*": ["./src/app/services/*"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | --------------------------------------------------------------------------------