├── .buckconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .expo ├── packager-info.json └── settings.json ├── .flowconfig ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ └── build.yml ├── .gitignore ├── .gitlab-ci.yml ├── .prettierignore ├── .prettierrc ├── App.js ├── README.md ├── android ├── .gitignore ├── app │ ├── build.gradle │ ├── debug.keystore │ ├── proguard-rules.pro │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── wednesdaysolutions │ │ │ └── rntws │ │ │ ├── MainActivity.kt │ │ │ └── MainApplication.kt │ │ └── res │ │ ├── drawable-hdpi │ │ └── splashscreen_image.png │ │ ├── drawable-mdpi │ │ └── splashscreen_image.png │ │ ├── drawable-xhdpi │ │ └── splashscreen_image.png │ │ ├── drawable-xxhdpi │ │ └── splashscreen_image.png │ │ ├── drawable-xxxhdpi │ │ └── splashscreen_image.png │ │ ├── drawable │ │ ├── rn_edit_text_material.xml │ │ └── splashscreen.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_foreground.png │ │ ├── values-night │ │ └── colors.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sentry.properties └── settings.gradle ├── app.json ├── app ├── app.js ├── assets │ └── images │ │ ├── logo.png │ │ ├── wednesday-logo-new.png │ │ ├── wednesday-logo-old.png │ │ ├── wednesday-logo.png │ │ └── wednesday-logo.svg ├── components │ ├── atoms │ │ ├── Container │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.js.snap │ │ │ │ └── index.test.js │ │ ├── If │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.js.snap │ │ │ │ └── index.test.js │ │ ├── LanguageProvider │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ └── index.test.js │ │ └── T │ │ │ ├── index.js │ │ │ └── tests │ │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ │ └── index.test.js │ ├── molecules │ │ ├── CharacterWithQuote │ │ │ ├── index.js │ │ │ └── tests │ │ │ │ ├── __snapshots__ │ │ │ │ └── index.test.js.snap │ │ │ │ └── index.test.js │ │ └── LogoWithInstructions │ │ │ ├── index.js │ │ │ └── tests │ │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ │ └── index.test.js │ └── organisms │ │ └── SimpsonsLoveWednesday │ │ ├── index.js │ │ └── tests │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ └── index.test.js ├── config │ ├── index.dev.js │ ├── index.js │ └── index.production.js ├── i18n.js ├── i18n.test.js ├── navigators │ └── appNavigator.js ├── package.json ├── scenes │ ├── ExampleScreen │ │ ├── index.js │ │ ├── recoilState.js │ │ └── tests │ │ │ ├── index.test.js │ │ │ └── recoilState.test.js │ ├── RootScreen │ │ ├── index.js │ │ ├── recoilState.js │ │ └── tests │ │ │ ├── __snapshots__ │ │ │ └── index.test.js.snap │ │ │ └── index.test.js │ └── SplashScreen │ │ ├── index.js │ │ └── tests │ │ ├── __snapshots__ │ │ └── index.test.js.snap │ │ └── index.test.js ├── services │ ├── navigationService.js │ ├── tests │ │ ├── navigate.test.js │ │ ├── navigateAndReset.test.js │ │ ├── setTopLevelNavigator.test.js │ │ └── userService.test.js │ └── userService.js ├── themes │ ├── colors.js │ ├── fonts.js │ ├── fonts.test.js │ ├── images.js │ └── index.js ├── translations │ └── en.json └── utils │ ├── apiUtils.js │ ├── common.js │ ├── constants.js │ ├── errors.js │ ├── growthbook.js │ ├── i18nextTestUtils.js │ ├── posthogEvents.js │ ├── posthogUtils.js │ └── testUtils.js ├── assets ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── eas.json ├── index.js ├── ios ├── .gitignore ├── .xcode.env ├── Podfile ├── Podfile.lock ├── Podfile.properties.json ├── reactnativetemplatews.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── reactnativetemplatews.xcscheme ├── reactnativetemplatews.xcworkspace │ └── contents.xcworkspacedata ├── reactnativetemplatews │ ├── AppDelegate.h │ ├── AppDelegate.mm │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── App-Icon-1024x1024@1x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── SplashScreen.imageset │ │ │ ├── Contents.json │ │ │ └── image.png │ │ └── SplashScreenBackground.imageset │ │ │ ├── Contents.json │ │ │ └── image.png │ ├── Info.plist │ ├── SplashScreen.storyboard │ ├── Supporting │ │ └── Expo.plist │ ├── main.m │ ├── noop-file.swift │ ├── reactnativetemplatews-Bridging-Header.h │ └── reactnativetemplatews.entitlements └── sentry.properties ├── jestconfig.js ├── jsconfig.json ├── metro.config.js ├── package.json ├── react_native_template_github.svg ├── setupTests.js ├── sonar-project.properties ├── web-build └── register-service-worker.js ├── webpack.config.js └── yarn.lock /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | JSON_PLACEHOLDER_API="https://jsonplaceholder.typicode.com" 2 | SIMPSONS_API="https://thesimpsonsquoteapi.glitch.me/" 3 | SENTRY_DSN= "YOU_SENTRY_DSN" 4 | POSTHOG_KEY= 'YOUR_POSTHOG_PROJECT_KEY' -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | android/** 3 | ios/** 4 | __tests__/** 5 | **/tests/*** 6 | web-build/** 7 | .eslintrc.js 8 | e2e/**/*.* 9 | metrics/* 10 | jest.setup.js 11 | babel.config.js 12 | reports 13 | report.json 14 | growthbook.js 15 | **/tests/*.test.js 16 | webpack.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | const prettierOptions = JSON.parse( 5 | fs.readFileSync(path.resolve(__dirname, '.prettierrc'), 'utf8') 6 | ); 7 | module.exports = { 8 | root: true, 9 | parser: 'babel-eslint', 10 | extends: [ 11 | 'airbnb', 12 | 'prettier', 13 | 'prettier/react', 14 | 'plugin:prettier/recommended', 15 | 'plugin:sonarjs/recommended', 16 | 'plugin:security/recommended-legacy', 17 | 'plugin:fp/recommended' 18 | ], 19 | plugins: [ 20 | 'github', 21 | 'immutable', 22 | 'sonarjs', 23 | 'prettier', 24 | 'react-native', 25 | 'react', 26 | 'react-hooks', 27 | 'jsx-a11y', 28 | 'fp' 29 | ], 30 | env: { 31 | jest: true, 32 | browser: true, 33 | node: true, 34 | es6: true 35 | }, 36 | parserOptions: { 37 | ecmaVersion: 6, 38 | sourceType: 'module', 39 | ecmaFeatures: { 40 | jsx: true 41 | } 42 | }, 43 | rules: { 44 | 'prettier/prettier': ['error', prettierOptions], 45 | 'arrow-body-style': [2, 'as-needed'], 46 | 'class-methods-use-this': 0, 47 | 'import/imports-first': 0, 48 | 'import/newline-after-import': 0, 49 | 'import/no-dynamic-require': 0, 50 | 'import/no-extraneous-dependencies': 0, 51 | 'import/no-named-as-default': 0, 52 | 'import/no-unresolved': 0, 53 | 'import/prefer-default-export': 0, 54 | 'react/jsx-props-no-spreading': 0, 55 | camelcase: ['error', { properties: 'always', ignoreImports: false }], 56 | indent: [ 57 | 2, 58 | 2, 59 | { 60 | SwitchCase: 1 61 | } 62 | ], 63 | 'jsx-a11y/aria-props': 2, 64 | 'jsx-a11y/heading-has-content': 0, 65 | 'jsx-a11y/label-has-associated-control': [ 66 | 2, 67 | { 68 | // NOTE: If this error triggers, either disable it or add 69 | // your custom components, labels and attributes via these options 70 | // See https://github.com/evcohen/eslint-plugin-jsx-a11y/blob/master/docs/rules/label-has-associated-control.md 71 | controlComponents: ['Input'] 72 | } 73 | ], 74 | 'jsx-a11y/label-has-for': 0, 75 | 'jsx-a11y/mouse-events-have-key-events': 2, 76 | 'jsx-a11y/role-has-required-aria-props': 2, 77 | 'jsx-a11y/role-supports-aria-props': 2, 78 | 'max-len': 0, 79 | 'newline-per-chained-call': 0, 80 | 'no-confusing-arrow': 0, 81 | 'no-console': 1, 82 | 'no-unused-vars': 2, 83 | 'no-use-before-define': 0, 84 | 'prefer-template': 2, 85 | 'react/destructuring-assignment': 0, 86 | 'react-hooks/rules-of-hooks': 'error', 87 | 'react/jsx-closing-tag-location': 0, 88 | 'react/forbid-prop-types': 0, 89 | 'react/jsx-first-prop-new-line': [2, 'multiline'], 90 | 'react/jsx-filename-extension': 0, 91 | 'react/jsx-no-target-blank': 0, 92 | 'react/jsx-uses-vars': 2, 93 | 'react/require-default-props': 0, 94 | 'react/require-extension': 0, 95 | 'react/self-closing-comp': 0, 96 | 'react/sort-comp': 0, 97 | 'require-yield': 0, 98 | 'react/no-array-index-key': 0, 99 | 'react/jsx-curly-newline': 0, 100 | 'react/jsx-one-expression-per-line': 0, 101 | 'react/jsx-wrap-multilines': 0, 102 | 'react/no-unused-prop-types': 0, 103 | 'max-lines-per-function': ['error', 250], 104 | 'no-else-return': 'error', 105 | 'max-params': ['error', 3], 106 | 'require-jsdoc': [ 107 | 'error', 108 | { 109 | require: { 110 | FunctionDeclaration: true, 111 | MethodDefinition: false, 112 | ClassDeclaration: false, 113 | ArrowFunctionExpression: false, 114 | FunctionExpression: false 115 | } 116 | } 117 | ], 118 | 'no-shadow': 'error', 119 | complexity: ['error', 4], 120 | 'no-empty': 'error', 121 | 'import/order': [ 122 | 'error', 123 | { 124 | groups: [ 125 | ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'] 126 | ] 127 | } 128 | ], 129 | 'immutable/no-let': 2, 130 | 'immutable/no-this': 2, 131 | 'max-lines': ['error', 350], 132 | 'react-native/no-unused-styles': 2, 133 | 'react-native/split-platform-components': 2, 134 | 'react-native/no-inline-styles': 2, 135 | 'react-native/no-color-literals': 2, 136 | 'react-native/no-raw-text': 2, 137 | 'react-native/no-single-element-style-arrays': 2, 138 | 'fp/no-mutation': [ 139 | 'error', 140 | { 141 | exceptions: [{ property: 'propTypes' }, { property: 'defaultProps' }] 142 | } 143 | ], 144 | 'fp/no-nil': 0, 145 | 'fp/no-unused-expression': 0, 146 | 'fp/no-throw': 0 147 | }, 148 | settings: { 149 | 'import/resolver': { 150 | node: { 151 | app: './app', 152 | context: 'app', 153 | resolve: { 154 | alias: { 155 | '@assets': './app/assets', 156 | '@components': './app/components', 157 | '@containers': './app/containers', 158 | '@config': './app/config', 159 | '@navigators': './app/navigators', 160 | '@services': './app/services', 161 | '@themes': './app/themes', 162 | '@utils': './app/utils' 163 | }, 164 | paths: ['app'], 165 | modules: ['app', 'node_modules'], 166 | extensions: ['.js', '.jsx', '.json', '.coffee', '.cjsx'] 167 | } 168 | } 169 | } 170 | } 171 | }; 172 | -------------------------------------------------------------------------------- /.expo/packager-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "devToolsPort": 19002, 3 | "expoServerPort": 19000, 4 | "packagerPort": 19000, 5 | "packagerPid": null, 6 | "expoServerNgrokUrl": null, 7 | "packagerNgrokUrl": null, 8 | "ngrokPid": null, 9 | "webpackServerPort": null 10 | } 11 | -------------------------------------------------------------------------------- /.expo/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "hostType": "lan", 3 | "lanType": "ip", 4 | "dev": true, 5 | "minify": false, 6 | "urlRandomness": null, 7 | "https": false, 8 | "scheme": null, 9 | "devClient": false 10 | } 11 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | node_modules/react-native/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | node_modules/react-native/Libraries/polyfills/.* 18 | 19 | ; These should not be required directly 20 | ; require from fbjs/lib instead: require('fbjs/lib/warning') 21 | node_modules/warning/.* 22 | 23 | ; Flow doesn't support platforms 24 | .*/Libraries/Utilities/HMRLoadingView.js 25 | 26 | [untyped] 27 | .*/node_modules/@react-native-community/cli/.*/.* 28 | 29 | [include] 30 | 31 | [libs] 32 | node_modules/react-native/Libraries/react-native/react-native-interface.js 33 | node_modules/react-native/flow/ 34 | 35 | [options] 36 | emoji=true 37 | 38 | esproposal.optional_chaining=enable 39 | esproposal.nullish_coalescing=enable 40 | 41 | module.file_ext=.js 42 | module.file_ext=.json 43 | module.file_ext=.ios.js 44 | 45 | module.system=haste 46 | module.system.haste.use_name_reducers=true 47 | # get basename 48 | module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' 49 | # strip .js or .js.flow suffix 50 | module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' 51 | # strip .ios suffix 52 | module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' 53 | module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' 54 | module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' 55 | module.system.haste.paths.blacklist=.*/__tests__/.* 56 | module.system.haste.paths.blacklist=.*/__mocks__/.* 57 | module.system.haste.paths.whitelist=/node_modules/react-native/Libraries/.* 58 | module.system.haste.paths.whitelist=/node_modules/react-native/RNTester/.* 59 | module.system.haste.paths.whitelist=/node_modules/react-native/IntegrationTests/.* 60 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/react-native/react-native-implementation.js 61 | module.system.haste.paths.blacklist=/node_modules/react-native/Libraries/Animated/src/polyfills/.* 62 | 63 | munge_underscores=true 64 | 65 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 66 | 67 | suppress_type=$FlowIssue 68 | suppress_type=$FlowFixMe 69 | suppress_type=$FlowFixMeProps 70 | suppress_type=$FlowFixMeState 71 | 72 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\) 73 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(\\)? *\\(site=[a-z,_]*react_native\\(_ios\\)?_\\(oss\\|fb\\)[a-z,_]*\\)?)\\)?:? #[0-9]+ 74 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 75 | 76 | [lints] 77 | sketchy-null-number=warn 78 | sketchy-null-mixed=warn 79 | sketchy-number=warn 80 | untyped-type-import=warn 81 | nonstrict-import=warn 82 | deprecated-type=warn 83 | unsafe-getters-setters=warn 84 | inexact-spread=warn 85 | unnecessary-invariant=warn 86 | signature-verification-failure=warn 87 | deprecated-utility=error 88 | 89 | [strict] 90 | deprecated-type 91 | nonstrict-import 92 | sketchy-null 93 | unclear-type 94 | unsafe-getters-setters 95 | untyped-import 96 | untyped-type-import 97 | 98 | [version] 99 | ^0.98.0 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | * text=auto 3 | 4 | # Force the following filetypes to have unix eols, so Windows does not break them 5 | **/*.* text eol=lf 6 | 7 | # Windows forced line-endings 8 | /.idea/* text eol=crlf 9 | 10 | # 11 | ## These files are binary and should be left untouched 12 | # 13 | 14 | # (binary is a macro for -text -diff) 15 | *.png binary 16 | *.jpg binary 17 | *.jpeg binary 18 | *.gif binary 19 | *.ico binary 20 | *.mov binary 21 | *.mp4 binary 22 | *.mp3 binary 23 | *.flv binary 24 | *.fla binary 25 | *.swf binary 26 | *.gz binary 27 | *.zip binary 28 | *.7z binary 29 | *.ttf binary 30 | *.eot binary 31 | *.woff binary 32 | *.woff2 binary 33 | *.pyc binary 34 | *.pdf binary 35 | *.ez binary 36 | *.bz2 binary 37 | *.swp binary 38 | *.swp binary 39 | *.ipa binary 40 | *.apk binary 41 | *.jar binary 42 | *gradlew binary 43 | *BUCK binary 44 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Ticket Link 2 | --------------------------------------------------- 3 | 4 | 5 | ### Related Links 6 | --------------------------------------------------- 7 | 8 | 9 | ### Description 10 | --------------------------------------------------- 11 | 12 | 13 | ### Steps to Reproduce / Test 14 | --------------------------------------------------- 15 | 16 | 17 | ### GIF's 18 | --------------------------------------------------- 19 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: react-native-template 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master, dev] 7 | jobs: 8 | install-and-test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Install npm dependencies 13 | run: yarn 14 | 15 | - name: Lint 16 | run: yarn lint 17 | 18 | - name: Test and generate coverage report 19 | uses: artiomtr/jest-coverage-report-action@v2.2.9 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | threshold: 80 23 | package-manager: yarn 24 | skip-step: install 25 | test-script: yarn test 26 | 27 | - name: SonarQube Scan 28 | uses: sonarsource/sonarqube-scan-action@master 29 | env: 30 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 31 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 32 | 33 | - uses: sonarsource/sonarqube-quality-gate-action@master 34 | timeout-minutes: 5 35 | env: 36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 37 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | web-build/** 8 | build/ 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata 18 | *.xccheckout 19 | *.moved-aside 20 | DerivedData 21 | *.hmap 22 | *.ipa 23 | *.xcuserstate 24 | project.xcworkspace 25 | 26 | # Android/IntelliJ 27 | # 28 | build/ 29 | .idea 30 | .gradle 31 | local.properties 32 | *.iml 33 | 34 | # node.js 35 | # 36 | node_modules/ 37 | npm-debug.log 38 | yarn-error.log 39 | 40 | # BUCK 41 | buck-out/ 42 | \.buckd/ 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | /coverage/ 61 | .expo/ 62 | web-build/ 63 | dist/ 64 | reports 65 | coverage 66 | .env.local 67 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:8.10.0 2 | 3 | cache: 4 | paths: 5 | - node_modules 6 | 7 | before_script: 8 | - yarn 9 | 10 | prettier: 11 | script: 12 | - npm run prettier-check 13 | 14 | eslint: 15 | script: 16 | - npm run lint-check 17 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/** 2 | android/** 3 | ios/** 4 | __tests__/** -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import { registerRootComponent } from 'expo'; 6 | import App from '@app/app'; 7 | import { SENTRY_DSN } from '@env'; 8 | import * as Sentry from '@sentry/react-native'; 9 | 10 | Sentry.init({ 11 | dsn: SENTRY_DSN 12 | }); 13 | 14 | if (!window.Intl) { 15 | new Promise(resolve => { 16 | resolve(import('intl')); 17 | }) 18 | .then(() => Promise.all([import('intl/locale-data/jsonp/en')])) 19 | .then(() => registerRootComponent(App)) 20 | .catch(alert); 21 | } else { 22 | registerRootComponent(App); 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |

8 |

React Native Template 9 |

10 |

11 | 12 |

13 | An enterprise React Native template application showcasing - Testing strategies, Global state management, middleware support, a network layer, component library integration, localization, navigation configuration, Continuous integration, analytics, feature flagging, and error tracking. 14 |

15 | 16 | --- 17 | 18 |

19 |

20 | Expert teams of digital product strategists, developers, and designers. 21 |

22 |

23 | 24 | 32 | 33 | --- 34 | 35 | We’re always looking for people who value their work, so come and join us. We are hiring! 36 | 37 |
38 | 39 | ## Architecture 40 | 41 | The driving goal of the architecture of the template is separation of concerns. Namely: 42 | 43 | - **Presentational components are separated from scenes** (aka "screens"). 44 | 45 | Presentational components are small components that are concerned with _how things look_. Scenes usually define whole application screens and are concerned with _how things work_: they include presentational components and wire everything together. 46 | 47 | If you are interested you can [read more about it here](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0). 48 | 49 | ### Atomic Design for react native architecture 50 | 51 | Atomic design further solidifies the idea of separating screens into components and scenes (containers). The design primarily focuses on reusability of code, which brings us to the differentiation of components into atoms, molecules, and organisms. Analogous to the Atomic design of chemicals, components are separated by their composition. The components require increasing context as their complexity increases, since each component is tested, this promotes a more granular test coverage. 52 | 53 | - **Atoms** 54 | Atoms are the smallest components that can be reused. Button, Text, and Icons are good examples of Atoms. Atoms can be used without context and cannot be further divided. 55 | 56 | - **Molecules** 57 | Molecules are built from one or more atoms that are slightly complex presentational components. 58 | 59 | - **Organisms** 60 | Organisms contain multiple molecules, atoms, and perform a specific purpose. In the example screen, an organism is used that displays the fetched character and quote. 61 | 62 | - **State is managed using [Recoil](https://recoiljs.org/)**. 63 | 64 | Recoil provides a set of utilities to manage global state in React Native. It allows for atoms (the smallest units of state) and selectors (to transform or combine state) to handle the app's state efficiently. This eliminates the need for Redux, actions, and reducers, simplifying the process for managing state across components. 65 | 66 | Atoms are the core units of state, and selectors are derived state values computed from one or more atoms. Recoil's state management is highly reactive and more efficient for handling state at a granular level. 67 | 68 | If you are interested you can [read more about it here](https://recoiljs.org/docs/introduction/getting-started). 69 | 70 | - **Side Effects (API calls, etc.) are managed within components or with Recoil selectors**. 71 | 72 | Recoil allows for managing side effects within the components themselves or through asynchronous selectors. This keeps your side effects closer to where they are needed. 73 | 74 | ## Analytics, Feature Flagging, and Error Tracking 75 | 76 | - **[PostHog](https://posthog.com/)** is integrated to provide analytics and event tracking across the application. PostHog captures user interactions and events, which helps in analyzing user behavior and improving the app based on data-driven insights. 77 | 78 | - **[GrowthBook](https://www.growthbook.io/)** is used for feature flagging. With GrowthBook, you can easily manage the rollout of features to users, enabling A/B testing, and controlling which features are visible to which segments of your user base without redeploying the app. 79 | 80 | - **[Sentry](https://sentry.io/)** is used for error tracking and reporting. Sentry captures errors and exceptions from the app in real time, providing detailed insights into the errors that occur, enabling faster bug fixing and better app stability. 81 | 82 | ## Content 83 | 84 | The React Native Template contains: 85 | 86 | - a [React Native](https://facebook.github.io/react-native/) (v**0.73.6**) application (in "[ejected](https://github.com/react-community/create-react-native-app/blob/master/EJECTING.md)" mode to allow using dependencies that rely on native code) 87 | - a [clear directory layout](#directory-layout) to provide a base architecture for your application 88 | - [Recoil](https://recoiljs.org/) to manage global state 89 | - [React Navigation](https://reactnavigation.org/) (v5.3.15) with a [`NavigationService`](app/services/navigationService.js) to handle routing and navigation in the app, with a splash screen setup by default 90 | - [axios](https://github.com/axios/axios/) to make API calls (v0.27.2) 91 | - [PostHog](https://posthog.com/) for analytics 92 | - [GrowthBook](https://www.growthbook.io/) for feature flagging 93 | - [Sentry](https://sentry.io/) for error tracking 94 | - [prettier](https://prettier.io/) and [eslint](https://eslint.org/) preconfigured for React Native 95 | 96 | The template includes an example (displaying fake user data) from UI components to state management using Recoil. The example is easy to remove so that it doesn't get in the way. 97 | 98 | ## Directory layout 99 | 100 | - [`app/components`](app/components): presentational components 101 | - [`app/components/atoms`](app/components/atoms): smallest components 102 | - [`app/components/molecules`](app/components/molecules): molecules are a group of one or more atoms 103 | - [`app/components/organisms`](app/components/organisms): organisms are one or more molecules 104 | - [`app/scenes`](app/components/scenes): scenes are screens that can be navigated to 105 | - [`app/config`](app/config): configuration of the application 106 | - [`app/assets`](app/assets): assets (image, audio files, ...) used by the application 107 | - [`app/navigators`](app/navigators): react navigation navigators 108 | - [`app/services`](app/services): application services, e.g. API clients 109 | - [`app/utils`](app/utils): Util methods and constants 110 | - [`app/themes`](app/themes): base styles for the application 111 | 112 | For more information on each directory, click the link and read the directory's README. 113 | 114 | ## Requirements 115 | 116 | Node 8 or greater is required. Development for iOS requires a Mac and Xcode 9 or up, and will target iOS 9 and up. 117 | 118 | You also need to install the dependencies required by React Native: 119 | 120 | - for [Android development](https://reactnative.dev/docs/set-up-your-environment?platform=android) 121 | - for [iOS development](https://reactnative.dev/docs/set-up-your-environment?platform=ios) 122 | 123 | ## Using the template 124 | 125 | To create a new project using the template: 126 | 127 | - clone this repository 128 | - remove the previous git history: `yarn initialize` 129 | - install the npm dependencies by running `yarn` 130 | - rename the React Native project to your own project name: `yarn run rename -- ` (the default name is `ReactNativeApplication`) 131 | - remove the LICENSE file and the "License" section from the README if your project is not open source 132 | 133 | ### Running expo project 134 | 135 | ### Android 136 | 137 | - `yarn run android` 138 | 139 | ### iOS 140 | 141 | - `yarn run ios` 142 | 143 | ## Useful documentation 144 | 145 | ### Deployment 146 | 147 | - Using [Fastlane](https://fastlane.tools/) to automate builds and store deployments (iOS and Android) 148 | - [Distributing beta builds](docs/beta%20builds.md) 149 | 150 | ### Package dependencies 151 | 152 | - You may want to use [CocoaPods](https://cocoapods.org/) to manage your dependencies (iOS only) 153 | - [Using CocoaPods to manage your package dependencies](docs/setup%20cocoapods.md) 154 | -------------------------------------------------------------------------------- /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 | 14 | # Bundle artifacts 15 | *.jsbundle 16 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | apply plugin: "org.jetbrains.kotlin.android" 3 | apply plugin: "com.facebook.react" 4 | 5 | def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() 6 | 7 | /** 8 | * This is the configuration block to customize your React Native Android app. 9 | * By default you don't need to apply any configuration, just uncomment the lines you need. 10 | */ 11 | react { 12 | entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim()) 13 | reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() 14 | hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" 15 | codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() 16 | 17 | // Use Expo CLI to bundle the app, this ensures the Metro config 18 | // works correctly with Expo projects. 19 | cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) 20 | bundleCommand = "export:embed" 21 | 22 | /* Folders */ 23 | // The root of your project, i.e. where "package.json" lives. Default is '..' 24 | // root = file("../") 25 | // The folder where the react-native NPM package is. Default is ../node_modules/react-native 26 | // reactNativeDir = file("../node_modules/react-native") 27 | // The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen 28 | // codegenDir = file("../node_modules/@react-native/codegen") 29 | 30 | /* Variants */ 31 | // The list of variants to that are debuggable. For those we're going to 32 | // skip the bundling of the JS bundle and the assets. By default is just 'debug'. 33 | // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. 34 | // debuggableVariants = ["liteDebug", "prodDebug"] 35 | 36 | /* Bundling */ 37 | // A list containing the node command and its flags. Default is just 'node'. 38 | // nodeExecutableAndArgs = ["node"] 39 | 40 | // 41 | // The path to the CLI configuration file. Default is empty. 42 | // bundleConfig = file(../rn-cli.config.js) 43 | // 44 | // The name of the generated asset file containing your JS bundle 45 | // bundleAssetName = "MyApplication.android.bundle" 46 | // 47 | // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' 48 | // entryFile = file("../js/MyApplication.android.js") 49 | // 50 | // A list of extra flags to pass to the 'bundle' commands. 51 | // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle 52 | // extraPackagerArgs = [] 53 | 54 | /* Hermes Commands */ 55 | // The hermes compiler command to run. By default it is 'hermesc' 56 | // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" 57 | // 58 | // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" 59 | // hermesFlags = ["-O", "-output-source-map"] 60 | } 61 | 62 | /** 63 | * Set this to true to Run Proguard on Release builds to minify the Java bytecode. 64 | */ 65 | def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() 66 | 67 | /** 68 | * The preferred build flavor of JavaScriptCore (JSC) 69 | * 70 | * For example, to use the international variant, you can use: 71 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` 72 | * 73 | * The international variant includes ICU i18n library and necessary data 74 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that 75 | * give correct results when using with locales other than en-US. Note that 76 | * this variant is about 6MiB larger per architecture than default. 77 | */ 78 | def jscFlavor = 'org.webkit:android-jsc:+' 79 | 80 | apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle") 81 | android { 82 | ndkVersion rootProject.ext.ndkVersion 83 | 84 | buildToolsVersion rootProject.ext.buildToolsVersion 85 | compileSdk rootProject.ext.compileSdkVersion 86 | 87 | namespace 'com.wednesdaysolutions.rntws' 88 | defaultConfig { 89 | applicationId 'com.wednesdaysolutions.rntws' 90 | minSdkVersion rootProject.ext.minSdkVersion 91 | targetSdkVersion rootProject.ext.targetSdkVersion 92 | versionCode 1 93 | versionName "1.0.0" 94 | 95 | buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) 96 | } 97 | signingConfigs { 98 | debug { 99 | storeFile file('debug.keystore') 100 | storePassword 'android' 101 | keyAlias 'androiddebugkey' 102 | keyPassword 'android' 103 | } 104 | } 105 | buildTypes { 106 | debug { 107 | signingConfig signingConfigs.debug 108 | } 109 | release { 110 | // Caution! In production, you need to generate your own keystore file. 111 | // see https://reactnative.dev/docs/signed-apk-android. 112 | signingConfig signingConfigs.debug 113 | shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) 114 | minifyEnabled enableProguardInReleaseBuilds 115 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 116 | } 117 | } 118 | packagingOptions { 119 | jniLibs { 120 | useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) 121 | } 122 | } 123 | } 124 | 125 | // Apply static values from `gradle.properties` to the `android.packagingOptions` 126 | // Accepts values in comma delimited lists, example: 127 | // android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini 128 | ["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop -> 129 | // Split option: 'foo,bar' -> ['foo', 'bar'] 130 | def options = (findProperty("android.packagingOptions.$prop") ?: "").split(","); 131 | // Trim all elements in place. 132 | for (i in 0.. 0) { 137 | println "android.packagingOptions.$prop += $options ($options.length)" 138 | // Ex: android.packagingOptions.pickFirsts += '**/SCCS/**' 139 | options.each { 140 | android.packagingOptions[prop] += it 141 | } 142 | } 143 | } 144 | 145 | dependencies { 146 | // The version of react-native is set by the React Native Gradle Plugin 147 | implementation("com.facebook.react:react-android") 148 | 149 | def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true"; 150 | def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true"; 151 | def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true"; 152 | 153 | if (isGifEnabled) { 154 | // For animated gif support 155 | implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") 156 | } 157 | 158 | if (isWebpEnabled) { 159 | // For webp support 160 | implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") 161 | if (isWebpAnimatedEnabled) { 162 | // Animated webp support 163 | implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") 164 | } 165 | } 166 | 167 | implementation("com.facebook.react:flipper-integration") 168 | 169 | if (hermesEnabled.toBoolean()) { 170 | implementation("com.facebook.react:hermes-android") 171 | } else { 172 | implementation jscFlavor 173 | } 174 | } 175 | 176 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 177 | applyNativeModulesAppBuildGradle(project) -------------------------------------------------------------------------------- /android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/debug.keystore -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.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 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/wednesdaysolutions/rntws/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.wednesdaysolutions.rntws 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | 6 | import com.facebook.react.ReactActivity 7 | import com.facebook.react.ReactActivityDelegate 8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 9 | import com.facebook.react.defaults.DefaultReactActivityDelegate 10 | 11 | import expo.modules.ReactActivityDelegateWrapper 12 | 13 | class MainActivity : ReactActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | // Set the theme to AppTheme BEFORE onCreate to support 16 | // coloring the background, status bar, and navigation bar. 17 | // This is required for expo-splash-screen. 18 | setTheme(R.style.AppTheme); 19 | super.onCreate(null) 20 | } 21 | 22 | /** 23 | * Returns the name of the main component registered from JavaScript. This is used to schedule 24 | * rendering of the component. 25 | */ 26 | override fun getMainComponentName(): String = "main" 27 | 28 | /** 29 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 30 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 31 | */ 32 | override fun createReactActivityDelegate(): ReactActivityDelegate { 33 | return ReactActivityDelegateWrapper( 34 | this, 35 | BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, 36 | object : DefaultReactActivityDelegate( 37 | this, 38 | mainComponentName, 39 | fabricEnabled 40 | ){}) 41 | } 42 | 43 | /** 44 | * Align the back button behavior with Android S 45 | * where moving root activities to background instead of finishing activities. 46 | * @see onBackPressed 47 | */ 48 | override fun invokeDefaultOnBackPressed() { 49 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { 50 | if (!moveTaskToBack(false)) { 51 | // For non-root activities, use the default implementation to finish them. 52 | super.invokeDefaultOnBackPressed() 53 | } 54 | return 55 | } 56 | 57 | // Use the default back button implementation on Android S 58 | // because it's doing more than [Activity.moveTaskToBack] in fact. 59 | super.invokeDefaultOnBackPressed() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/wednesdaysolutions/rntws/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.wednesdaysolutions.rntws 2 | 3 | import android.app.Application 4 | import android.content.res.Configuration 5 | import androidx.annotation.NonNull 6 | 7 | import com.facebook.react.PackageList 8 | import com.facebook.react.ReactApplication 9 | import com.facebook.react.ReactNativeHost 10 | import com.facebook.react.ReactPackage 11 | import com.facebook.react.ReactHost 12 | import com.facebook.react.config.ReactFeatureFlags 13 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 14 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 15 | import com.facebook.react.defaults.DefaultReactNativeHost 16 | import com.facebook.react.flipper.ReactNativeFlipper 17 | import com.facebook.soloader.SoLoader 18 | 19 | import expo.modules.ApplicationLifecycleDispatcher 20 | import expo.modules.ReactNativeHostWrapper 21 | 22 | class MainApplication : Application(), ReactApplication { 23 | 24 | override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper( 25 | this, 26 | object : DefaultReactNativeHost(this) { 27 | override fun getPackages(): List { 28 | // Packages that cannot be autolinked yet can be added manually here, for example: 29 | // packages.add(new MyReactNativePackage()); 30 | return PackageList(this).packages 31 | } 32 | 33 | override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" 34 | 35 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 36 | 37 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 38 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 39 | } 40 | ) 41 | 42 | override val reactHost: ReactHost 43 | get() = getDefaultReactHost(this.applicationContext, reactNativeHost) 44 | 45 | override fun onCreate() { 46 | super.onCreate() 47 | SoLoader.init(this, false) 48 | if (!BuildConfig.REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS) { 49 | ReactFeatureFlags.unstable_useRuntimeSchedulerAlways = false 50 | } 51 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 52 | // If you opted-in for the New Architecture, we load the native entry point for this app. 53 | load() 54 | } 55 | if (BuildConfig.DEBUG) { 56 | ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager) 57 | } 58 | ApplicationLifecycleDispatcher.onApplicationCreate(this) 59 | } 60 | 61 | override fun onConfigurationChanged(newConfig: Configuration) { 62 | super.onConfigurationChanged(newConfig) 63 | ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/drawable-hdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/drawable-mdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/drawable-xhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/drawable-xxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/drawable-xxxhdpi/splashscreen_image.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/splashscreen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff 3 | #FFFFFF 4 | #023c69 5 | #ffffff 6 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | react-native-template-ws 3 | contain 4 | false 5 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 14 | 17 | -------------------------------------------------------------------------------- /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') ?: '34.0.0' 6 | minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23') 7 | compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34') 8 | targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34') 9 | kotlinVersion = findProperty('android.kotlinVersion') ?: '1.8.10' 10 | 11 | ndkVersion = "25.1.8937393" 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 | } 21 | } 22 | 23 | apply plugin: "com.facebook.react.rootproject" 24 | 25 | allprojects { 26 | repositories { 27 | maven { 28 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 29 | url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android')) 30 | } 31 | maven { 32 | // Android JSC is installed from npm 33 | url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist')) 34 | } 35 | 36 | google() 37 | mavenCentral() 38 | maven { url 'https://www.jitpack.io' } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=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=false 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 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /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 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /android/sentry.properties: -------------------------------------------------------------------------------- 1 | 2 | auth.token=YOUR_SENTRY_AUTH_TOKEN 3 | 4 | defaults.org=YOUR_ORG_NAME 5 | defaults.project=YOUR_PROJECT_NAME 6 | 7 | defaults.url=https://sentry.io/ 8 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'react-native-template-ws' 2 | 3 | dependencyResolutionManagement { 4 | versionCatalogs { 5 | reactAndroidLibs { 6 | from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml"))) 7 | } 8 | } 9 | } 10 | 11 | apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle"); 12 | useExpoModules() 13 | 14 | apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle"); 15 | applyNativeModulesSettingsGradle(settings) 16 | 17 | include ':app' 18 | includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) 19 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-template-ws", 4 | "slug": "react-native-template-cd", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": ["**/*"], 17 | "web": { 18 | "favicon": "./assets/favicon.png" 19 | }, 20 | "android": { 21 | "package": "com.wednesdaysolutions.rntws" 22 | }, 23 | "ios": { 24 | "bundleIdentifier": "com.wednesdaysolutions.rntws" 25 | }, 26 | "plugins": [ 27 | [ 28 | "@sentry/react-native/expo", 29 | { 30 | "url": "https://sentry.io/", 31 | "project": "react-native", 32 | "organization": "wednesday-solutions-5p" 33 | } 34 | ] 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot } from 'recoil'; 3 | import { I18nextProvider } from 'react-i18next'; 4 | import 'react-native-gesture-handler'; 5 | import LanguageProvider from '@atoms/LanguageProvider'; 6 | import RootScreen from '@scenes/RootScreen'; 7 | import i18n from '@app/i18n'; 8 | 9 | const App = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /app/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/app/assets/images/logo.png -------------------------------------------------------------------------------- /app/assets/images/wednesday-logo-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/app/assets/images/wednesday-logo-new.png -------------------------------------------------------------------------------- /app/assets/images/wednesday-logo-old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/app/assets/images/wednesday-logo-old.png -------------------------------------------------------------------------------- /app/assets/images/wednesday-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/app/assets/images/wednesday-logo.png -------------------------------------------------------------------------------- /app/assets/images/wednesday-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/components/atoms/Container/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Container 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import styled from 'styled-components/native'; 9 | 10 | const StyledContainer = styled.View` 11 | display: flex; 12 | flex: 1; 13 | `; 14 | const Container = ({ ...props }) => ( 15 | 16 | ); 17 | 18 | Container.propTypes = {}; 19 | 20 | export default Container; 21 | -------------------------------------------------------------------------------- /app/components/atoms/Container/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render and match the snapshot 1`] = ` 4 | 17 | `; 18 | -------------------------------------------------------------------------------- /app/components/atoms/Container/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for Container 4 | * 5 | */ 6 | import React from 'react'; 7 | import { renderWithI18next } from 'app/utils/testUtils'; 8 | import Container from '../index'; 9 | 10 | describe('', () => { 11 | it('should render and match the snapshot', () => { 12 | const baseElement = renderWithI18next(); 13 | expect(baseElement).toMatchSnapshot(); 14 | }); 15 | 16 | it('should contain 1 container', () => { 17 | const { getAllByTestId } = renderWithI18next(); 18 | expect(getAllByTestId('container').length).toBe(1); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/atoms/If/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * If 4 | * 5 | */ 6 | // eslint-disable-next-line 7 | import React from 'react'; 8 | import Proptypes from 'prop-types'; 9 | const If = props => (props.condition ? props.children : props.otherwise); 10 | // eslint-disable-next-line fp/no-mutation 11 | If.propTypes = { 12 | condition: Proptypes.bool, 13 | otherwise: Proptypes.oneOfType([ 14 | Proptypes.arrayOf(Proptypes.node), 15 | Proptypes.node 16 | ]), 17 | children: Proptypes.oneOfType([ 18 | Proptypes.arrayOf(Proptypes.node), 19 | Proptypes.node 20 | ]) 21 | }; 22 | If.defaultProps = { 23 | otherwise: null 24 | }; 25 | export default If; 26 | -------------------------------------------------------------------------------- /app/components/atoms/If/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` Should render and match the snapshot 1`] = `null`; 4 | -------------------------------------------------------------------------------- /app/components/atoms/If/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for If 4 | * 5 | * @see https://github.com/react-boilerplate/react-boilerplate/tree/master/docs/testing 6 | * 7 | */ 8 | 9 | import React from 'react'; 10 | import { Text } from 'react-native'; 11 | import { render } from '@testing-library/react-native'; 12 | import If from '../index'; 13 | 14 | describe('', () => { 15 | it('Should render and match the snapshot', () => { 16 | const baseElement = render(); 17 | expect(baseElement).toMatchSnapshot(); 18 | }); 19 | 20 | it('If should render the child component when props.condition = true', () => { 21 | const conditionTrueText = 'Should render when condition is true'; 22 | const conditionFalseText = 'Should render condition is false'; 23 | const OtherwiseComponent = () => {conditionFalseText}; 24 | const TrueConditionComponent = () => {conditionTrueText}; 25 | const props = { 26 | otherwise: , 27 | condition: true 28 | }; 29 | const { queryByText, getByText } = render( 30 | 31 | 32 | 33 | ); 34 | expect(getByText(conditionTrueText)).toBeTruthy(); 35 | expect(queryByText(conditionFalseText)).not.toBeTruthy(); 36 | }); 37 | 38 | it('If should render the component passed on otherwise when props.condition = false', () => { 39 | const conditionFalseText = 'Should render condition is false'; 40 | const conditionTrueText = 'Should render when condition is true'; 41 | const TrueConditionComponent = () => {conditionTrueText}; 42 | const OtherwiseComponent = () => {conditionFalseText}; 43 | const props = { 44 | otherwise: , 45 | condition: false 46 | }; 47 | 48 | const { queryByText, getByText } = render( 49 | 50 | 51 | 52 | ); 53 | expect(getByText(conditionFalseText)).toBeTruthy(); 54 | expect(queryByText(conditionTrueText)).not.toBeTruthy(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /app/components/atoms/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | /** 6 | * Provides internationalization (i18n) support by ensuring that the necessary 7 | * translations and locale information are available throughout the app. 8 | * 9 | * @param {object} props - The props object containing component properties. 10 | * @param {React.ReactNode} props.children - The child elements/components to be rendered. 11 | * @returns {React.ReactNode} A JSX element wrapping the provided child components. 12 | */ 13 | export function LanguageProvider({ children }) { 14 | useTranslation(); // This initializes the i18next context for this component tree 15 | 16 | return <>{React.Children.only(children)}; 17 | } 18 | 19 | LanguageProvider.propTypes = { 20 | children: PropTypes.element.isRequired 21 | }; 22 | 23 | export default LanguageProvider; 24 | -------------------------------------------------------------------------------- /app/components/atoms/LanguageProvider/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react-native'; 3 | import T from '@atoms/T'; 4 | import { renderWithI18next } from '@utils/testUtils'; 5 | import { Text } from 'react-native'; 6 | import ConnectedLanguageProvider, { LanguageProvider } from '../index'; 7 | describe(' container tests', () => { 8 | it('should render its children', () => { 9 | const children = ( 10 |

11 | Test 12 |

13 | ); 14 | const container = renderWithI18next( 15 | {children} 16 | ); 17 | expect(container.firstChild).not.toBeNull(); 18 | }); 19 | }); 20 | 21 | describe(' container tests', () => { 22 | it('should render the default language messages', () => { 23 | const { queryByText } = render( 24 | 25 | 26 | 27 | ); 28 | expect(queryByText('because')).not.toBeNull(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /app/components/atoms/T/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * T 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { Text } from 'react-native'; 10 | 11 | import { useTranslation } from 'react-i18next'; 12 | import { conditionalOperatorFunction } from '@app/utils/common'; 13 | 14 | const T = ({ intl, id, values, style, text, ...otherProps }) => { 15 | const { t } = useTranslation(); 16 | return ( 17 | 18 | {conditionalOperatorFunction(id, t(id, { ...values }), text)} 19 | 20 | ); 21 | }; 22 | 23 | T.propTypes = { 24 | id: PropTypes.string, 25 | intl: PropTypes.object, 26 | values: PropTypes.object, 27 | style: PropTypes.arrayOf(PropTypes.object), 28 | text: PropTypes.string 29 | }; 30 | T.defaultProps = { 31 | text: '' 32 | }; 33 | 34 | export default T; 35 | -------------------------------------------------------------------------------- /app/components/atoms/T/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render and match the snapshot 1`] = ` 4 | 7 | `; 8 | -------------------------------------------------------------------------------- /app/components/atoms/T/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for T 4 | * 5 | */ 6 | import React from 'react'; 7 | import { renderWithI18next } from 'app/utils/testUtils'; 8 | import T from '../index'; 9 | 10 | describe('', () => { 11 | it('should render and match the snapshot', () => { 12 | const baseElement = renderWithI18next(); 13 | expect(baseElement).toMatchSnapshot(); 14 | }); 15 | 16 | it('should contain 1 t', () => { 17 | const { getAllByTestId } = renderWithI18next(); 18 | expect(getAllByTestId('t').length).toBe(1); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/components/molecules/CharacterWithQuote/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/native'; 3 | import get from 'lodash/get'; 4 | import PropTypes from 'prop-types'; 5 | import { fonts } from '@themes'; 6 | import T from '@atoms/T'; 7 | 8 | const Result = styled(T)` 9 | ${fonts.style.standard()}; 10 | text-align: center; 11 | margin-bottom: 5; 12 | `; 13 | 14 | const CharacterImage = styled.Image` 15 | height: 80px; 16 | width: 80px; 17 | margin: 0 auto; 18 | `; 19 | 20 | /** 21 | * Renders a component displaying a character with associated quote and image. 22 | * 23 | * @component 24 | * @param {Object} props - The props object. 25 | * @param {Object} props.user - The user object containing character and quote information. 26 | * @param {string} props.user.character - The character name to display. 27 | * @param {string} props.user.image - The URL of the image associated with the character. 28 | * @param {string} props.user.quote - The quote associated with the character. 29 | * @returns {JSX.Element} A React element representing the character display with quote. 30 | */ 31 | function CharacterWithQuote({ user }) { 32 | return ( 33 | <> 34 | 40 | 41 | 46 | 47 | 48 | ); 49 | } 50 | 51 | CharacterWithQuote.propTypes = { 52 | user: PropTypes.object 53 | }; 54 | 55 | export default CharacterWithQuote; 56 | -------------------------------------------------------------------------------- /app/components/molecules/CharacterWithQuote/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` Should render and match the snapshot 1`] = ` 4 | [ 5 | 17 | wednesday_lover 18 | , 19 | 31 | because 32 | , 33 | , 54 | , 66 | ] 67 | `; 68 | -------------------------------------------------------------------------------- /app/components/molecules/CharacterWithQuote/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for CharacterWithQuote 4 | * 5 | * @see https://github.com/react-boilerplate/react-boilerplate/tree/master/docs/testing 6 | * 7 | */ 8 | 9 | import React from 'react'; 10 | import get from 'lodash/get'; 11 | import { renderWithI18next } from '@utils/testUtils'; 12 | import CharacterWithQuote from '../index'; 13 | 14 | describe('', () => { 15 | it('Should render and match the snapshot', () => { 16 | const baseElement = renderWithI18next(); 17 | expect(baseElement).toMatchSnapshot(); 18 | }); 19 | 20 | it('Should render the Character name, image and quote provided as the user prop', () => { 21 | const props = { 22 | user: { 23 | character: 'Homer', 24 | image: 25 | 'https://www.onthisday.com/images/people/homer-simpson-medium.jpg', 26 | quote: "D'Oh!" 27 | } 28 | }; 29 | const { getByText, getByTestId } = renderWithI18next( 30 | 31 | ); 32 | expect(getByText('wednesday_lover')).toBeTruthy(); 33 | expect(getByText(props.user.quote)).toBeTruthy(); 34 | const characterImageURI = get( 35 | getByTestId('character-image'), 36 | '_fiber.pendingProps.source.uri' 37 | ); 38 | expect(characterImageURI).toBe(props.user.image); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /app/components/molecules/LogoWithInstructions/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Image } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | import PropTypes from 'prop-types'; 5 | import { fonts, images } from '@themes'; 6 | import T from '@atoms/T'; 7 | 8 | const styles = { 9 | parentView: { 10 | marginTop: 30, 11 | marginBottom: 30 12 | }, 13 | logoContainer: { 14 | width: '100%', 15 | height: 150, 16 | marginBottom: 25 17 | }, 18 | logo: { 19 | width: '100%', 20 | height: '100%', 21 | alignSelf: 'center' 22 | } 23 | }; 24 | 25 | const Instructions = styled(T)` 26 | ${fonts.style.standard()}; 27 | text-align: center; 28 | margin-bottom: 5; 29 | font-style: italic; 30 | `; 31 | /** 32 | * A component that displays a logo along with instructions. 33 | * @param {object} props - The props object containing component properties. 34 | * @param {string} props.instructions - The instructions text to display. 35 | * @returns {React.ReactNode} JSX elements displaying the logo and instructions. 36 | */ 37 | function LogoWithInstructions({ instructions }) { 38 | return ( 39 | 40 | 41 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | LogoWithInstructions.propTypes = { 53 | instructions: PropTypes.string 54 | }; 55 | 56 | export default LogoWithInstructions; 57 | -------------------------------------------------------------------------------- /app/components/molecules/LogoWithInstructions/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` Should render and match the snapshot 1`] = ` 4 | 5 | 14 | 29 | 30 | 43 | 44 | `; 45 | -------------------------------------------------------------------------------- /app/components/molecules/LogoWithInstructions/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for LogoWithInstructions 4 | * 5 | * @see https://github.com/react-boilerplate/react-boilerplate/tree/master/docs/testing 6 | * 7 | */ 8 | 9 | import React from 'react'; 10 | import { renderWithI18next } from '@utils/testUtils'; 11 | import LogoWithInstructions from '../index'; 12 | 13 | describe('', () => { 14 | it('Should render and match the snapshot', () => { 15 | const baseElement = renderWithI18next(); 16 | expect(baseElement).toMatchSnapshot(); 17 | }); 18 | it('should render the instructions pased as props', () => { 19 | const instructions = 'PRESS CMD + D for iOS'; 20 | const { getByText } = renderWithI18next( 21 | 22 | ); 23 | expect(getByText(instructions)).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /app/components/organisms/SimpsonsLoveWednesday/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled from 'styled-components/native'; 4 | import { fonts } from '@themes'; 5 | import If from '@app/components/atoms/If'; 6 | import CharacterWithQuote from '@molecules/CharacterWithQuote'; 7 | import LogoWithInstructions from '@molecules/LogoWithInstructions'; 8 | 9 | const Err = styled.Text` 10 | ${fonts.style.standard()}; 11 | text-align: center; 12 | margin-bottom: 5px; 13 | color: red; 14 | `; 15 | 16 | const SeparatedView = styled.View` 17 | > * { 18 | margin: 10px; 19 | } 20 | `; 21 | /** 22 | * A component that displays Simpsons-themed content related to Wednesday, including instructions, character details, and error messages. 23 | * @param {object} props - The props object containing component properties. 24 | * @param {string} props.instructions - The instructions text to display. 25 | * @param {object} props.user - The user object representing character details and quote. 26 | * @param {string} props.userErrorMessage - The error message to display if user data retrieval fails. 27 | * @returns {React.ReactNode} JSX elements displaying Simpsons-themed content based on provided props. 28 | */ 29 | function SimpsonsLoveWednesday({ instructions, user, userErrorMessage }) { 30 | return ( 31 | <> 32 | 33 | {userErrorMessage}} 36 | > 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | SimpsonsLoveWednesday.propTypes = { 46 | user: PropTypes.object, 47 | instructions: PropTypes.string, 48 | userErrorMessage: PropTypes.string 49 | }; 50 | 51 | export default SimpsonsLoveWednesday; 52 | -------------------------------------------------------------------------------- /app/components/organisms/SimpsonsLoveWednesday/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` Should render and match the snapshot 1`] = ` 4 | [ 5 | 6 | 15 | 30 | 31 | 44 | , 45 | 52 | 64 | wednesday_lover 65 | 66 | 78 | because 79 | 80 | 101 | 113 | , 114 | ] 115 | `; 116 | -------------------------------------------------------------------------------- /app/components/organisms/SimpsonsLoveWednesday/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for SimpsonsLoveWednesday 4 | * 5 | * @see https://github.com/react-boilerplate/react-boilerplate/tree/master/docs/testing 6 | * 7 | */ 8 | 9 | import React from 'react'; 10 | import { renderWithI18next } from '@utils/testUtils'; 11 | import { rerender } from '@testing-library/react-native'; 12 | import SimpsonsLoveWednesday from '../index'; 13 | describe('', () => { 14 | it('Should render and match the snapshot', () => { 15 | const baseElement = renderWithI18next(); 16 | expect(baseElement).toMatchSnapshot(); 17 | }); 18 | it('Should render the Error component if userErrorMessage is not empty', () => { 19 | const props = { 20 | userErrorMessage: 'Fetch failed', 21 | instructions: 'PRESS CMD + D for iOS', 22 | user: { 23 | character: 'Homer', 24 | image: 25 | 'https://www.onthisday.com/images/people/homer-simpson-medium.jpg', 26 | quote: "D'Oh!" 27 | } 28 | }; 29 | const { getByText } = renderWithI18next( 30 | 31 | ); 32 | expect(getByText(props.userErrorMessage)).toBeTruthy(); 33 | }); 34 | it('Should render the component if userErrorMessage is empty', () => { 35 | const props = { 36 | userErrorMessage: null, 37 | instructions: 'PRESS CMD + D for iOS', 38 | user: { 39 | character: 'Homer', 40 | image: 41 | 'https://www.onthisday.com/images/people/homer-simpson-medium.jpg', 42 | quote: "D'Oh!" 43 | } 44 | }; 45 | const { getByText: textQueryOnReRender } = renderWithI18next( 46 | , 47 | rerender 48 | ); 49 | expect(textQueryOnReRender(`wednesday_lover`)).toBeTruthy(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /app/config/index.dev.js: -------------------------------------------------------------------------------- 1 | import { JSON_PLACEHOLDER_API } from '@env'; 2 | 3 | export const Config = { 4 | API_URL: `${JSON_PLACEHOLDER_API}/users/` 5 | }; 6 | -------------------------------------------------------------------------------- /app/config/index.js: -------------------------------------------------------------------------------- 1 | import { SIMPSONS_API } from '@env'; 2 | 3 | export const Config = { 4 | API_URL: SIMPSONS_API 5 | }; 6 | -------------------------------------------------------------------------------- /app/config/index.production.js: -------------------------------------------------------------------------------- 1 | import { JSON_PLACEHOLDER_API } from '@env'; 2 | 3 | export const Config = { 4 | API_URL: `${JSON_PLACEHOLDER_API}/users/` 5 | }; 6 | -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | /* eslint-disable fp/no-mutating-assign */ 4 | /** 5 | * i18n.js 6 | * 7 | * This will setup the i18n language files and locale data for your app. 8 | * 9 | * IMPORTANT: This file is used by the internal build 10 | * script `extract-intl`, and must use CommonJS module syntax 11 | * You CANNOT use import/export in this file. 12 | */ 13 | 14 | const enTranslationMessages = require('./translations/en.json'); 15 | 16 | export const languageDetector = { 17 | type: 'languageDetector', 18 | async: true, 19 | detect: cb => cb('en'), 20 | init: () => {}, 21 | cacheUserLanguage: () => {} 22 | }; 23 | 24 | i18n 25 | .use(languageDetector) 26 | .use(initReactI18next) 27 | .init({ 28 | fallbackLng: 'en', 29 | debug: false, 30 | resources: { 31 | en: { 32 | translation: enTranslationMessages 33 | } 34 | }, 35 | interpolation: { 36 | escapeValue: true 37 | } 38 | }); 39 | 40 | export default i18n; 41 | -------------------------------------------------------------------------------- /app/i18n.test.js: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import { languageDetector } from './i18n'; 4 | 5 | describe('i18n configuration', () => { 6 | it('should configure i18next with the correct settings', () => { 7 | // Import the i18n configuration 8 | 9 | // Verify that the language detector was used 10 | expect(i18next.use).toHaveBeenCalledWith( 11 | expect.objectContaining({ 12 | type: 'languageDetector', 13 | async: true, 14 | detect: expect.any(Function), 15 | init: expect.any(Function), 16 | cacheUserLanguage: expect.any(Function) 17 | }) 18 | ); 19 | 20 | // Verify that the initReactI18next was used 21 | expect(i18next.use).toHaveBeenCalledWith(initReactI18next); 22 | 23 | // Verify that i18next was initialized with the correct configuration 24 | expect(i18next.init).toHaveBeenCalledWith( 25 | expect.objectContaining({ 26 | fallbackLng: 'en', 27 | debug: false, 28 | resources: { 29 | en: { 30 | translation: expect.any(Object) // This should match the contents of enTranslationMessages 31 | } 32 | }, 33 | interpolation: { 34 | escapeValue: true 35 | } 36 | }) 37 | ); 38 | }); 39 | 40 | it('should detect language as "en" using the language detector', () => { 41 | // Call the detect function and ensure it was passed 'en' 42 | languageDetector.detect(language => { 43 | expect(language).toBe('en'); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /app/navigators/appNavigator.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PostHogProvider } from 'posthog-react-native'; 3 | import { createStackNavigator } from '@react-navigation/stack'; 4 | import SplashScreen from '@scenes/SplashScreen/'; 5 | import ExampleScreen from '@scenes/ExampleScreen'; 6 | import { NavigationContainer } from '@react-navigation/native'; 7 | import { setTopLevelNavigator } from '@services/navigationService'; 8 | import { getPostHogClient } from '@app/utils/posthogUtils'; 9 | const Stack = createStackNavigator(); 10 | /** 11 | * The root screen contains the application's navigation. 12 | * 13 | * @see https://reactnavigation.org/docs/en/hello-react-navigation.html#creating-a-stack-navigator 14 | */ 15 | export default function AppNavigator() { 16 | return ( 17 | 18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app" 3 | } 4 | -------------------------------------------------------------------------------- /app/scenes/ExampleScreen/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Button, Platform, View, ActivityIndicator } from 'react-native'; 3 | import { 4 | useRecoilState, 5 | useSetRecoilState, 6 | useRecoilValueLoadable 7 | } from 'recoil'; 8 | import styled from 'styled-components/native'; 9 | import { useTranslation } from 'react-i18next'; 10 | import { usePostHog } from 'posthog-react-native'; 11 | 12 | import AppContainer from '@atoms/Container'; 13 | import SimpsonsLoveWednesday from '@organisms/SimpsonsLoveWednesday'; 14 | import If from '@app/components/atoms/If'; 15 | import { conditionalOperatorFunction } from '@app/utils/common'; 16 | import { LoadingStates } from '@app/utils/constants'; 17 | import { POSTHOG_EVENTS } from '@app/utils/posthogEvents'; 18 | 19 | import { userState, fetchUserSelector, fetchTriggerState } from './recoilState'; 20 | 21 | const Container = styled(AppContainer)` 22 | margin: 30px; 23 | flex: 1; 24 | justify-content: center; 25 | align-items: center; 26 | max-width: 320px; 27 | align-self: center; 28 | `; 29 | 30 | const CustomButtonParentView = styled(View)` 31 | margin-top: 40px; 32 | max-width: 80px; 33 | align-self: center; 34 | `; 35 | 36 | const ExampleScreen = () => { 37 | const [user, setUser] = useRecoilState(userState); 38 | const setFetchTrigger = useSetRecoilState(fetchTriggerState); 39 | const userLoadable = useRecoilValueLoadable(fetchUserSelector); 40 | const posthog = usePostHog(); 41 | const { t } = useTranslation(); 42 | const requestFetchUser = () => { 43 | setFetchTrigger(prev => prev + 1); 44 | }; 45 | const instructions = Platform.select({ 46 | ios: t('ios_instructions'), 47 | android: t('android_instructions') 48 | }); 49 | 50 | useEffect(() => { 51 | requestFetchUser(); 52 | }, []); 53 | 54 | useEffect(() => { 55 | if (userLoadable.state === LoadingStates.HAS_VALUE) { 56 | setUser(userLoadable.contents); 57 | } 58 | }, [userLoadable?.contents?.character]); 59 | 60 | const refreshButtonHandler = () => { 61 | posthog.capture(POSTHOG_EVENTS.REFRESH_BUTTON_CLICKED); 62 | requestFetchUser(); 63 | }; 64 | 65 | return ( 66 | 67 | 71 | 80 | 81 | 85 | 86 |
87 | } 88 | > 89 | 90 | 91 |
92 | ); 93 | }; 94 | 95 | export default ExampleScreen; 96 | export { ExampleScreen as ExampleScreenTest }; 97 | -------------------------------------------------------------------------------- /app/scenes/ExampleScreen/recoilState.js: -------------------------------------------------------------------------------- 1 | import { atom, selector } from 'recoil'; 2 | import { getUser } from '@app/services/userService'; 3 | import { Errors } from '@app/utils/errors'; 4 | import { errorHandlerFunction } from '@app/utils/common'; 5 | 6 | const stateKeys = { 7 | USER_STATE: 'userState', 8 | USER_IS_LOADING_STATE: 'userIsLoadingState', 9 | USER_ERROR_MESSAGE_STATE: 'userErrorMessageState', 10 | FETCH_TRIGGER_STATE: 'fetchTriggerState', 11 | FETCH_USER_SELECTOR: 'fetchUserSelector' 12 | }; 13 | 14 | // Atom to manage user state 15 | export const userState = atom({ 16 | key: stateKeys.USER_STATE, 17 | default: null 18 | }); 19 | 20 | // Atom to manage loading state 21 | export const userIsLoadingState = atom({ 22 | key: stateKeys.USER_IS_LOADING_STATE, 23 | default: false 24 | }); 25 | 26 | // Atom to manage error messages 27 | export const userErrorMessageState = atom({ 28 | key: stateKeys.USER_ERROR_MESSAGE_STATE, 29 | default: null 30 | }); 31 | 32 | // Atom to trigger a fetch 33 | export const fetchTriggerState = atom({ 34 | key: stateKeys.FETCH_TRIGGER_STATE, 35 | default: 0 // This will be incremented to trigger re-fetching 36 | }); 37 | 38 | // Selector to fetch user data 39 | export const fetchUserSelector = selector({ 40 | key: stateKeys.FETCH_USER_SELECTOR, 41 | get: async ({ get }) => { 42 | get(fetchTriggerState); // Read the trigger state to force re-fetch 43 | 44 | const response = await getUser(); 45 | 46 | errorHandlerFunction(response, Errors.GET_USER_ERROR); 47 | const { data } = response; 48 | return data[0]; 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /app/scenes/ExampleScreen/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | RecoilRoot, 4 | useRecoilValueLoadable, 5 | useSetRecoilState, 6 | useRecoilState 7 | } from 'recoil'; 8 | import { render, fireEvent } from '@testing-library/react-native'; 9 | import { ExampleScreenTest } from '../index'; 10 | 11 | jest.mock('recoil', () => ({ 12 | ...jest.requireActual('recoil'), 13 | useRecoilValueLoadable: jest.fn(), 14 | useSetRecoilState: jest.fn(), 15 | useRecoilState: jest.fn() 16 | })); 17 | jest.mock('posthog-react-native', () => ({ 18 | usePostHog: jest.fn(() => ({ 19 | identify: jest.fn(), 20 | capture: jest.fn() 21 | })) 22 | })); 23 | describe('ExampleScreen', () => { 24 | const mockSetFetchTrigger = jest.fn(); 25 | const mockSetUser = jest.fn(); 26 | const mockUseRecoilState = jest.fn(); 27 | 28 | beforeEach(() => { 29 | useSetRecoilState.mockReturnValue(mockSetFetchTrigger); 30 | useRecoilValueLoadable.mockReturnValue({ state: 'loading' }); 31 | mockUseRecoilState.mockReturnValue([null, mockSetUser]); 32 | useRecoilState.mockImplementation(mockUseRecoilState); 33 | jest.spyOn(global.console, 'error').mockImplementation(() => {}); 34 | }); 35 | 36 | afterEach(() => { 37 | jest.clearAllMocks(); 38 | }); 39 | 40 | test('renders loading indicator initially', () => { 41 | const { getByTestId } = render( 42 | 43 | 44 | 45 | ); 46 | 47 | expect(getByTestId('loader')).toBeTruthy(); 48 | }); 49 | 50 | test('renders content when userLoadable.state is "hasValue"', () => { 51 | useRecoilValueLoadable.mockReturnValue({ 52 | state: 'hasValue', 53 | contents: { character: 'Homer' } 54 | }); 55 | 56 | const { getByTestId } = render( 57 | 58 | 59 | 60 | ); 61 | 62 | expect(getByTestId('example-container-content')).toBeTruthy(); 63 | }); 64 | 65 | test('renders error message when userLoadable.state is "hasError"', () => { 66 | useRecoilValueLoadable.mockReturnValue({ 67 | state: 'hasError', 68 | contents: { message: 'Error occurred' } 69 | }); 70 | 71 | const { getByText } = render( 72 | 73 | 74 | 75 | ); 76 | 77 | expect(getByText('Error occurred')).toBeTruthy(); 78 | }); 79 | 80 | test('calls requestFetchUser on button press', () => { 81 | useRecoilValueLoadable.mockReturnValue({ 82 | state: 'hasValue', 83 | contents: { character: 'Homer' } 84 | }); 85 | 86 | const { getByText } = render( 87 | 88 | 89 | 90 | ); 91 | 92 | fireEvent.press(getByText('refresh')); 93 | expect(mockSetFetchTrigger).toHaveBeenCalled(); 94 | }); 95 | 96 | test('sets user when userLoadable.state is "hasValue"', () => { 97 | useRecoilValueLoadable.mockReturnValue({ 98 | state: 'hasValue', 99 | contents: { character: 'Homer' } 100 | }); 101 | 102 | render( 103 | 104 | 105 | 106 | ); 107 | 108 | expect(mockSetUser).toHaveBeenCalledWith({ character: 'Homer' }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /app/scenes/ExampleScreen/tests/recoilState.test.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; 3 | import TestRenderer from 'react-test-renderer'; 4 | import { getUser } from '@app/services/userService'; 5 | import { Errors } from '@app/utils/errors'; 6 | import { 7 | userState, 8 | userIsLoadingState, 9 | userErrorMessageState, 10 | fetchTriggerState, 11 | fetchUserSelector 12 | } from '../recoilState'; 13 | 14 | jest.mock('@app/services/userService'); 15 | 16 | describe('Recoil Atoms and Selector', () => { 17 | let testContainer = {}; 18 | 19 | const TestComponent = ({ atom, newValue }) => { 20 | const [value, setValue] = useRecoilState(atom); 21 | 22 | if (newValue !== undefined) { 23 | setValue(newValue); 24 | } 25 | 26 | testContainer = { value, setValue }; 27 | return null; 28 | }; 29 | 30 | const SelectorTestComponent = ({ selector }) => { 31 | const value = useRecoilValue(selector); 32 | testContainer = { value }; 33 | return null; 34 | }; 35 | 36 | beforeEach(() => { 37 | jest.clearAllMocks(); 38 | testContainer = {}; 39 | }); 40 | 41 | describe('userState atom', () => { 42 | it('should have a default value of null', () => { 43 | TestRenderer.create( 44 | 45 | 46 | 47 | ); 48 | expect(testContainer.value).toBeNull(); 49 | }); 50 | 51 | it('should update the user state', () => { 52 | TestRenderer.create( 53 | 54 | 55 | 56 | ); 57 | const { setValue } = testContainer; 58 | 59 | TestRenderer.act(() => { 60 | setValue({ id: 1, name: 'John Doe' }); 61 | }); 62 | 63 | expect(testContainer.value).toEqual({ id: 1, name: 'John Doe' }); 64 | }); 65 | }); 66 | 67 | describe('userIsLoadingState atom', () => { 68 | it('should have a default value of false', () => { 69 | TestRenderer.create( 70 | 71 | 72 | 73 | ); 74 | expect(testContainer.value).toBe(false); 75 | }); 76 | 77 | it('should update the loading state', () => { 78 | TestRenderer.create( 79 | 80 | 81 | 82 | ); 83 | const { setValue } = testContainer; 84 | 85 | TestRenderer.act(() => { 86 | setValue(true); 87 | }); 88 | 89 | expect(testContainer.value).toBe(true); 90 | }); 91 | }); 92 | 93 | describe('userErrorMessageState atom', () => { 94 | it('should have a default value of null', () => { 95 | TestRenderer.create( 96 | 97 | 98 | 99 | ); 100 | expect(testContainer.value).toBeNull(); 101 | }); 102 | 103 | it('should update the error message state', () => { 104 | TestRenderer.create( 105 | 106 | 107 | 108 | ); 109 | const { setValue } = testContainer; 110 | 111 | TestRenderer.act(() => { 112 | setValue('An error occurred'); 113 | }); 114 | 115 | expect(testContainer.value).toBe('An error occurred'); 116 | }); 117 | }); 118 | 119 | describe('fetchTriggerState atom', () => { 120 | it('should have a default value of 0', () => { 121 | TestRenderer.create( 122 | 123 | 124 | 125 | ); 126 | expect(testContainer.value).toBe(0); 127 | }); 128 | 129 | it('should increment the trigger state', () => { 130 | TestRenderer.create( 131 | 132 | 133 | 134 | ); 135 | const { setValue } = testContainer; 136 | 137 | TestRenderer.act(() => { 138 | setValue(prev => prev + 1); 139 | }); 140 | 141 | expect(testContainer.value).toBe(1); 142 | }); 143 | }); 144 | 145 | describe('fetchUserSelector', () => { 146 | it('should fetch and return user data when successful', async () => { 147 | const mockUser = [{ id: 1, name: 'John Doe' }]; 148 | getUser.mockResolvedValueOnce({ ok: true, data: mockUser }); 149 | 150 | await TestRenderer.act(async () => { 151 | TestRenderer.create( 152 | 153 | 154 | 155 | 156 | 157 | ); 158 | }); 159 | 160 | expect(testContainer.value).toEqual(mockUser[0]); 161 | }); 162 | 163 | it('should throw an error when the fetch fails', async () => { 164 | getUser.mockResolvedValueOnce({ ok: false }); 165 | 166 | await TestRenderer.act(async () => { 167 | try { 168 | TestRenderer.create( 169 | 170 | 171 | 172 | 173 | 174 | ); 175 | } catch (error) { 176 | expect(error.message).toBe(Errors.USER_FETCH_ERROR); 177 | } 178 | }); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /app/scenes/RootScreen/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import AppNavigator from '@navigators/appNavigator'; 4 | import Container from '@atoms/Container'; 5 | import { navigateAndReset } from '@app/services/navigationService'; 6 | import { appState } from './recoilState'; 7 | 8 | const RootScreen = () => { 9 | const app = useRecoilValue(appState); 10 | 11 | useEffect(() => { 12 | // Startup function 13 | if (!app) { 14 | setTimeout(() => navigateAndReset('MainScreen'), 1000); 15 | } 16 | }, [app]); 17 | 18 | return ( 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default RootScreen; 26 | export { RootScreen as RootScreenTest }; 27 | -------------------------------------------------------------------------------- /app/scenes/RootScreen/recoilState.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'recoil'; 2 | 3 | const stateKeys = { 4 | APP_STATE: 'appState' 5 | }; 6 | 7 | export const appState = atom({ 8 | key: stateKeys.APP_STATE, 9 | default: null 10 | }); 11 | -------------------------------------------------------------------------------- /app/scenes/RootScreen/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render and match the snapshot 1`] = `undefined`; 4 | -------------------------------------------------------------------------------- /app/scenes/RootScreen/tests/index.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useRecoilValue } from 'recoil'; 3 | import { render, waitFor } from '@testing-library/react-native'; 4 | import { navigateAndReset } from '@app/services/navigationService'; 5 | import { RootScreenTest } from '../index'; 6 | import { PostHogProvider } from 'posthog-react-native'; 7 | 8 | jest.mock('recoil'); 9 | jest.mock('@app/services/navigationService'); 10 | jest.mock('@app/utils/posthogUtils'); 11 | jest.mock('posthog-react-native', () => ({ 12 | PostHogProvider: jest.fn(({ children }) => children) 13 | })); 14 | 15 | describe('', () => { 16 | beforeEach(() => { 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('should render and match the snapshot', () => { 21 | useRecoilValue.mockReturnValue(true); // Mock app state as truthy 22 | const { baseElement } = render(); 23 | expect(baseElement).toMatchSnapshot(); 24 | }); 25 | 26 | it('should render the Container and AppNavigator components', () => { 27 | useRecoilValue.mockReturnValue(true); // Mock app state as truthy 28 | const { getByTestId } = render(); 29 | 30 | const rootScreenContainer = getByTestId('root-screen'); 31 | expect(rootScreenContainer).toBeTruthy(); 32 | 33 | // Check that AppNavigator is rendered within Container 34 | expect(rootScreenContainer.children.length).toBeGreaterThan(0); 35 | }); 36 | 37 | it('should call navigateAndReset when appState is falsy', async () => { 38 | useRecoilValue.mockReturnValue(null); // Mock app state as falsy 39 | render(); 40 | 41 | await waitFor( 42 | () => { 43 | expect(navigateAndReset).toHaveBeenCalledWith('MainScreen'); 44 | }, 45 | { timeout: 1500 } 46 | ); 47 | }); 48 | 49 | it('should not call navigateAndReset when appState is truthy', async () => { 50 | useRecoilValue.mockReturnValue(true); // Mock app state as truthy 51 | render(); 52 | 53 | await waitFor(() => { 54 | expect(navigateAndReset).not.toHaveBeenCalled(); 55 | }); 56 | }); 57 | 58 | it('should handle changes to appState', async () => { 59 | const { rerender } = render(); 60 | 61 | // First render with a falsy app state 62 | useRecoilValue.mockReturnValueOnce(null); 63 | rerender(); 64 | await waitFor( 65 | () => { 66 | expect(navigateAndReset).toHaveBeenCalledWith('MainScreen'); 67 | }, 68 | { timeout: 1500 } 69 | ); 70 | 71 | // Re-render with a truthy app state 72 | useRecoilValue.mockReturnValueOnce(true); 73 | rerender(); 74 | await waitFor(() => { 75 | expect(navigateAndReset).toHaveBeenCalledTimes(1); // Should not be called again 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /app/scenes/SplashScreen/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components/native'; 3 | import AppContainer from '@atoms/Container'; 4 | import { colors, images } from '@themes'; 5 | 6 | const Container = styled(AppContainer)` 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | background-color: ${colors.primary}; 11 | `; 12 | 13 | const Logo = styled.Image` 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | width: 200px; 18 | `; 19 | 20 | const SplashScreen = () => ( 21 | 22 | 23 | 24 | ); 25 | 26 | export default SplashScreen; 27 | -------------------------------------------------------------------------------- /app/scenes/SplashScreen/tests/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` should render and match the snapshot 1`] = ` 4 | 23 | 41 | 42 | `; 43 | -------------------------------------------------------------------------------- /app/scenes/SplashScreen/tests/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Tests for SplashScreen 4 | * 5 | */ 6 | import React from 'react'; 7 | import { renderWithI18next } from 'app/utils/testUtils'; 8 | import SplashScreen from '../index'; 9 | 10 | describe('', () => { 11 | it('should render and match the snapshot', () => { 12 | const baseElement = renderWithI18next(); 13 | expect(baseElement).toMatchSnapshot(); 14 | }); 15 | 16 | it('should contain 1 splash screen', () => { 17 | const { getAllByTestId } = renderWithI18next(); 18 | expect(getAllByTestId('splash-screen').length).toBe(1); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /app/services/navigationService.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable fp/no-mutating-assign */ 2 | import { NavigationActions, StackActions } from '@react-navigation/compat'; 3 | /** 4 | * The navigation is implemented as a service so that it can be used outside of components, for example in sagas. 5 | * 6 | * @see https://reactnavigation.org/docs/en/navigating-without-navigation-prop.html 7 | */ 8 | 9 | const navigatorObject = { 10 | navigator: null 11 | }; 12 | 13 | /** 14 | * This function is called when the RootScreen is created to set the navigator instance to use. 15 | */ 16 | export function setTopLevelNavigator(navigatorRef) { 17 | Object.assign(navigatorObject, { navigator: navigatorRef }); 18 | } 19 | 20 | /** 21 | * Call this function when you want to navigate to a specific route. 22 | * 23 | * @param routeName The name of the route to navigate to. Routes are defined in RootScreen using createStackNavigator() 24 | * @param params Route parameters. 25 | */ 26 | export function navigate(routeName, params) { 27 | navigatorObject.navigator.dispatch( 28 | NavigationActions.navigate({ 29 | routeName, 30 | params 31 | }) 32 | ); 33 | } 34 | 35 | /** 36 | * Call this function when you want to navigate to a specific route AND reset the navigation history. 37 | * 38 | * That means the user cannot go back. This is useful for example to redirect from a splashscreen to 39 | * the main screen: the user should not be able to go back to the splashscreen. 40 | * 41 | * @param routeName The name of the route to navigate to. Routes are defined in RootScreen using createStackNavigator() 42 | * @param params Route parameters. 43 | */ 44 | export function navigateAndReset(routeName, params) { 45 | navigatorObject.navigator.dispatch( 46 | StackActions.replace({ 47 | routeName, 48 | params 49 | }) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/services/tests/navigate.test.js: -------------------------------------------------------------------------------- 1 | import { NavigationActions } from '@react-navigation/compat'; 2 | import { navigate, setTopLevelNavigator } from '@services/navigationService'; 3 | jest.mock('@react-navigation/compat', () => ({ 4 | NavigationActions: { 5 | navigate: jest.fn() 6 | } 7 | })); 8 | const navigatorRef = { goBack: 'goBack', dispatch: jest.fn() }; 9 | setTopLevelNavigator(navigatorRef); 10 | describe('navigate', () => { 11 | afterEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | it('dispatches navigation action with the correct routeName and params', () => { 16 | const routeName = '/test'; 17 | const params = { screen: 'MainScreen' }; 18 | NavigationActions.navigate.mockReturnValueOnce({ 19 | type: 'NAVIGATE_ACTION', 20 | payload: { routeName, params } 21 | }); 22 | navigate(routeName, params); 23 | expect(NavigationActions.navigate).toHaveBeenCalledWith({ 24 | routeName, 25 | params 26 | }); 27 | expect(navigatorRef.dispatch).toHaveBeenCalledWith({ 28 | type: 'NAVIGATE_ACTION', 29 | payload: { routeName, params } 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/services/tests/navigateAndReset.test.js: -------------------------------------------------------------------------------- 1 | import { StackActions } from '@react-navigation/compat'; 2 | import { 3 | setTopLevelNavigator, 4 | navigateAndReset 5 | } from '@services/navigationService'; 6 | 7 | jest.mock('@react-navigation/compat', () => ({ 8 | StackActions: { 9 | replace: jest.fn() 10 | } 11 | })); 12 | const navigatorRef = { goBack: 'goBack', dispatch: jest.fn() }; 13 | setTopLevelNavigator(navigatorRef); 14 | describe('test navigateAndReset', () => { 15 | afterEach(() => { 16 | // Reset mocks after each test 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it('dispatches stack action with the correct routeName and params', () => { 21 | const routeName = '/test'; 22 | const params = { screen: 'MainScreen' }; 23 | StackActions.replace.mockReturnValueOnce({ 24 | type: 'NAVIGATE_ACTION', 25 | payload: { routeName, params } 26 | }); 27 | navigateAndReset(routeName, params); 28 | expect(StackActions.replace).toHaveBeenCalledWith({ 29 | routeName, 30 | params 31 | }); 32 | expect(navigatorRef.dispatch).toHaveBeenCalledWith({ 33 | type: 'NAVIGATE_ACTION', 34 | payload: { routeName, params } 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /app/services/tests/setTopLevelNavigator.test.js: -------------------------------------------------------------------------------- 1 | import { setTopLevelNavigator } from '@services/navigationService'; // Import the function to be tested 2 | 3 | describe('setTopLevelNavigator', () => { 4 | it('should update navigatorObject with the provided navigatorRef using mocked Object.assign', () => { 5 | const mockNavigatorRef = { navigator: 'test_navigator' }; 6 | const mockAssign = jest.spyOn(Object, 'assign'); 7 | setTopLevelNavigator(mockNavigatorRef); 8 | expect(mockAssign).toHaveBeenCalledTimes(1); 9 | mockAssign.mockRestore(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/services/tests/userService.test.js: -------------------------------------------------------------------------------- 1 | import MockAdapter from 'axios-mock-adapter'; 2 | import { generateApiClient } from 'app/utils/apiUtils'; 3 | import { getUser } from '../userService'; 4 | 5 | jest.mock('app/utils/apiUtils', () => ({ 6 | generateApiClient: jest.fn(() => ({ 7 | get: () => 8 | Promise.resolve({ 9 | data: [ 10 | { 11 | quote: 'Thank you. Come again.', 12 | character: 'Mohammed Ali Chherawalla', 13 | image: 14 | 'https://cdn.glitch.com/3c3ffadc-3406-4440-bb95-d40ec8fcde72%2FApuNahasapeemapetilon.png?1497567511629', 15 | characterDirection: 'Left' 16 | } 17 | ] 18 | }) 19 | })) 20 | })); 21 | 22 | describe('UserService tests', () => { 23 | it('should make the api call to "/quotes?count=1"', async () => { 24 | const data = [ 25 | { 26 | quote: 'Thank you. Come again.', 27 | character: 'Mohammed Ali Chherawalla', 28 | image: 29 | 'https://cdn.glitch.com/3c3ffadc-3406-4440-bb95-d40ec8fcde72%2FApuNahasapeemapetilon.png?1497567511629', 30 | characterDirection: 'Left' 31 | } 32 | ]; 33 | const res = await getUser(); 34 | expect(res.data).toEqual(data); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /app/services/userService.js: -------------------------------------------------------------------------------- 1 | import { set } from 'lodash'; 2 | import { generateApiClient } from '@app/utils/apiUtils'; 3 | 4 | const createApiClient = async () => generateApiClient('configApi'); 5 | const getApiClient = async () => { 6 | try { 7 | if (!client.apiClient) { 8 | set(client, 'apiClient', createApiClient()); 9 | } 10 | return client.apiClient; 11 | } catch (error) { 12 | throw new Error(error); 13 | } 14 | }; 15 | const client = { apiClient: null }; 16 | export const getUser = async () => { 17 | try { 18 | const apiClient = await getApiClient(); 19 | return apiClient.get('quotes?count=1'); 20 | } catch (error) { 21 | throw new Error(error); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /app/themes/colors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the application's colors. 3 | * 4 | * Define color here instead of duplicating them throughout the components. 5 | * That allows to change them more easily later on. 6 | */ 7 | 8 | export default { 9 | transparent: 'rgba(0,0,0,0)', 10 | // Example colors: 11 | text: '#212529', 12 | primary: '#fcedda', 13 | success: '#28a745', 14 | error: '#dc3545' 15 | }; 16 | -------------------------------------------------------------------------------- /app/themes/fonts.js: -------------------------------------------------------------------------------- 1 | import { css } from 'styled-components'; 2 | 3 | // sizes 4 | const regular = () => css` 5 | font-size: 17px; 6 | `; 7 | const small = () => css` 8 | font-size: 14px; 9 | `; 10 | const big = () => css` 11 | font-size: 20px; 12 | `; 13 | const large = () => css` 14 | font-size: 24px; 15 | `; 16 | 17 | // weights 18 | const light = () => css` 19 | font-weight: light; 20 | `; 21 | const bold = () => css` 22 | font-weight: bold; 23 | `; 24 | 25 | const normal = () => css` 26 | font-weight: normal; 27 | `; 28 | 29 | // styles 30 | const heading = () => css` 31 | ${large()}; 32 | ${bold()} 33 | `; 34 | 35 | const subheading = () => css` 36 | ${big()}; 37 | ${bold()} 38 | `; 39 | 40 | const standard = () => css` 41 | ${regular()}; 42 | ${normal()} 43 | `; 44 | 45 | const subText = () => css` 46 | ${small()}; 47 | ${normal()} 48 | `; 49 | 50 | export default { 51 | size: { 52 | regular, 53 | small, 54 | big, 55 | large 56 | }, 57 | style: { 58 | heading, 59 | subheading, 60 | standard, 61 | subText 62 | }, 63 | weights: { 64 | light, 65 | bold, 66 | normal 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /app/themes/fonts.test.js: -------------------------------------------------------------------------------- 1 | import fonts from './fonts'; 2 | const fontSizes = { 3 | small: { 4 | expectation: expect.arrayContaining([ 5 | expect.stringContaining('font-size: 14px;') 6 | ]) 7 | }, 8 | regular: { 9 | expectation: expect.arrayContaining([ 10 | expect.stringContaining('font-size: 17px;') 11 | ]) 12 | }, 13 | big: { 14 | expectation: expect.arrayContaining([ 15 | expect.stringContaining('font-size: 20px;') 16 | ]) 17 | }, 18 | large: { 19 | expectation: expect.arrayContaining([ 20 | expect.stringContaining('font-size: 24px;') 21 | ]) 22 | } 23 | }; 24 | 25 | const fontWeights = { 26 | light: { 27 | expectation: expect.arrayContaining([ 28 | expect.stringContaining('font-weight: light;') 29 | ]) 30 | }, 31 | bold: { 32 | expectation: expect.arrayContaining([ 33 | expect.stringContaining('font-weight: bold;') 34 | ]) 35 | }, 36 | normal: { 37 | expectation: expect.arrayContaining([ 38 | expect.stringContaining('font-weight: normal;') 39 | ]) 40 | } 41 | }; 42 | 43 | describe('Tests for fonts', () => { 44 | it('should have the correct font-size', () => { 45 | expect(fonts.size.small()).toEqual(fontSizes.small.expectation); 46 | expect(fonts.size.regular()).toEqual(fontSizes.regular.expectation); 47 | expect(fonts.size.big()).toEqual(fontSizes.big.expectation); 48 | expect(fonts.size.large()).toEqual(fontSizes.large.expectation); 49 | }); 50 | it('should have the correct font-weight', () => { 51 | expect(fonts.weights.light()).toEqual(fontWeights.light.expectation); 52 | expect(fonts.weights.bold()).toEqual(fontWeights.bold.expectation); 53 | expect(fonts.weights.normal()).toEqual(fontWeights.normal.expectation); 54 | }); 55 | 56 | it('should have the correct font-weight and font-size', () => { 57 | expect(fonts.style.heading()).toEqual(fontWeights.bold.expectation); 58 | expect(fonts.style.heading()).toEqual(fontSizes.large.expectation); 59 | 60 | expect(fonts.style.subheading()).toEqual(fontWeights.bold.expectation); 61 | expect(fonts.style.subheading()).toEqual(fontSizes.big.expectation); 62 | 63 | expect(fonts.style.standard()).toEqual(fontSizes.regular.expectation); 64 | expect(fonts.style.standard()).toEqual(fontWeights.normal.expectation); 65 | 66 | expect(fonts.style.subText()).toEqual(fontSizes.small.expectation); 67 | expect(fonts.style.subText()).toEqual(fontWeights.normal.expectation); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /app/themes/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Images should be stored in the `app/assets/images` directory and referenced using variables defined here. 3 | */ 4 | import logo from '@app/assets/images/logo.png'; 5 | import wednesdayLogo from '@app/assets/images/wednesday-logo-new.png'; 6 | 7 | export default { 8 | logo, 9 | wednesdayLogo 10 | }; 11 | -------------------------------------------------------------------------------- /app/themes/index.js: -------------------------------------------------------------------------------- 1 | import colors from './colors'; 2 | import fonts from './fonts'; 3 | import images from './images'; 4 | 5 | export { colors, fonts, images }; 6 | -------------------------------------------------------------------------------- /app/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "love_wednesday": "I love Wednesday and my name is {{username}}", 3 | "live_in_europe": "I live in europe", 4 | "dont_live_in_europe": "I don't live in europe", 5 | "because": "because", 6 | "wednesday_lover": "{{username}} loves Wednesday", 7 | "get_started": "To get started, edit App.js", 8 | "refresh": "Refresh", 9 | "ios_instructions": "Press Cmd+R to reload,\nCmd+D or shake for dev menu.", 10 | "'android_instructions": "Double tap R on your keyboard to reload,\nShake or press menu button for dev menu." 11 | } 12 | -------------------------------------------------------------------------------- /app/utils/apiUtils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable sonarjs/no-small-switch */ 2 | /* eslint-disable fp/no-mutating-assign */ 3 | import axios from 'axios'; 4 | import mapKeysDeep from 'map-keys-deep'; 5 | import camelCase from 'lodash/camelCase'; 6 | import snakeCase from 'lodash/snakeCase'; 7 | import get from 'lodash/get'; 8 | import { set } from 'lodash'; 9 | import { Config } from '@app/config/index'; 10 | 11 | export const apiClients = { 12 | configApi: null, 13 | default: null 14 | }; 15 | 16 | export const getApiClient = (type = 'configApi') => 17 | get(apiClients, type, apiClients.default); 18 | 19 | export const generateApiClient = (type = 'configApi') => { 20 | switch (type) { 21 | case 'configApi': 22 | set(apiClients, type, createApiClientWithTransForm(Config.API_URL)); 23 | 24 | return get(apiClients, type); 25 | default: 26 | set(apiClients, 'default', createApiClientWithTransForm(Config.API_URL)); 27 | return apiClients.default; 28 | } 29 | }; 30 | 31 | export const createApiClientWithTransForm = baseURL => { 32 | try { 33 | const api = axios.create({ 34 | baseURL, 35 | headers: { 'Content-Type': 'application/json' } 36 | }); 37 | 38 | // Response interceptor to transform keys to camelCase and structure response 39 | api.interceptors.response.use( 40 | response => { 41 | const { data } = response; 42 | if (data) { 43 | const keysData = mapKeysDeep(data, keys => camelCase(keys)); 44 | return { 45 | ok: true, 46 | data: keysData, 47 | error: null, 48 | originalResponse: response 49 | }; 50 | } 51 | return { 52 | ok: true, 53 | data: response.data, 54 | error: null, 55 | originalResponse: response 56 | }; 57 | }, 58 | error => ({ 59 | ok: false, 60 | data: null, 61 | error: error || 'Something went wrong', 62 | originalResponse: error.response 63 | }) 64 | ); 65 | 66 | // Request interceptor to transform keys to snake_case 67 | api.interceptors.request.use(request => { 68 | const { data } = request; 69 | if (data) { 70 | const keysData = mapKeysDeep(data, keys => snakeCase(keys)); 71 | return { ...request, data: keysData }; 72 | } 73 | return request; 74 | }); 75 | 76 | return api; 77 | } catch (err) { 78 | throw new Error(err); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /app/utils/common.js: -------------------------------------------------------------------------------- 1 | export const conditionalOperatorFunction = (condition, val1, val2) => 2 | condition ? val1 : val2; 3 | 4 | export const errorHandlerFunction = (response, error) => { 5 | if (!response.ok) { 6 | throw new Error(error); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /app/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const LoadingStates = { 2 | LOADING: 'loading', 3 | HAS_ERROR: 'hasError', 4 | HAS_VALUE: 'hasValue' 5 | }; 6 | -------------------------------------------------------------------------------- /app/utils/errors.js: -------------------------------------------------------------------------------- 1 | export const Errors = { 2 | USER_FETCH_ERROR: "'There was an error while fetching user information.'" 3 | }; 4 | -------------------------------------------------------------------------------- /app/utils/growthbook.js: -------------------------------------------------------------------------------- 1 | import { GrowthBook } from '@growthbook/growthbook'; 2 | import { GROWTH_BOOK_API_HOST, GROWTH_BOOK_CLIENT_KEY } from '@env'; 3 | 4 | let growthBookClient; 5 | 6 | /** 7 | * creates an instance of the growthBook client 8 | * @returns growthBook client 9 | */ 10 | export const createGrowthBookClient = userEmail => { 11 | const growthBook = new GrowthBook({ 12 | apiHost: GROWTH_BOOK_API_HOST, 13 | clientKey: GROWTH_BOOK_CLIENT_KEY, 14 | enableDevMode: true, 15 | subscribeToChanges: true 16 | }); 17 | 18 | growthBook.setAttributes({ 19 | email: userEmail 20 | }); 21 | 22 | return growthBook; 23 | }; 24 | 25 | /** 26 | * Function to get the GrowthBook client instance 27 | * @returns {GrowthBook} growthBook instance 28 | */ 29 | export function getGrowthBookClient(email) { 30 | if (growthBookClient) { 31 | return growthBookClient; 32 | } 33 | return createGrowthBookClient(email); 34 | } 35 | 36 | /** 37 | * Function to get growthBook feature 38 | * @param {String} email 39 | * @returns {Applicant} growthBook feature value 40 | */ 41 | export async function getGrowthBookFeaturesData(name, email) { 42 | try { 43 | const growthBook = getGrowthBookClient(email); 44 | await growthBook.loadFeatures(); 45 | return growthBook.getFeatureValue(name); 46 | } catch (error) { 47 | return new Error(error); 48 | } 49 | } 50 | 51 | /** 52 | * Function to get growthBook feature 53 | * @param {String} name 54 | * @returns {Boolean} growthBook feature status 55 | */ 56 | export async function getGrowthBookFeatureFlag(name, email) { 57 | try { 58 | const growthBook = getGrowthBookClient(email); 59 | await growthBook.loadFeatures(); 60 | return growthBook.isOn(name); 61 | } catch (error) { 62 | return new Error(error); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/utils/i18nextTestUtils.js: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | /* eslint-disable fp/no-mutating-assign */ 4 | /** 5 | * i18n.js 6 | * 7 | * This will setup the i18n language files and locale data for your app. 8 | * 9 | * IMPORTANT: This file is used by the internal build 10 | * script `extract-intl`, and must use CommonJS module syntax 11 | * You CANNOT use import/export in this file. 12 | */ 13 | // // const addLocaleData = require('react-intl').addLocaleData; //eslint-disable-line 14 | 15 | // // const enLocaleData = require('react-intl/locale-data/en'); 16 | 17 | const enTranslationMessages = require('@app/translations/en.json'); 18 | 19 | const languageDetector = { 20 | type: 'languageDetector', 21 | async: true, 22 | detect: cb => cb('en'), 23 | init: () => {}, 24 | cacheUserLanguage: () => {} 25 | }; 26 | 27 | i18next 28 | .use(languageDetector) 29 | .use(initReactI18next) 30 | .init({ 31 | fallbackLng: 'en', 32 | debug: true, 33 | resources: { 34 | en: { 35 | translation: enTranslationMessages // log this object to ensure it's correctly loaded 36 | } 37 | }, 38 | interpolation: { 39 | escapeValue: true // not needed for react!! 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /app/utils/posthogEvents.js: -------------------------------------------------------------------------------- 1 | export const POSTHOG_EVENTS = { 2 | REFRESH_BUTTON_CLICKED: 'refresh_button_clicked' 3 | }; 4 | -------------------------------------------------------------------------------- /app/utils/posthogUtils.js: -------------------------------------------------------------------------------- 1 | import { set } from 'lodash'; 2 | import PostHog from 'posthog-react-native'; 3 | import { POSTHOG_KEY } from '@env'; 4 | const posthog = { 5 | client: null 6 | }; 7 | 8 | export const getPostHogClient = () => { 9 | if (!posthog.client) { 10 | set( 11 | posthog, 12 | 'client', 13 | new PostHog(POSTHOG_KEY, { 14 | // In-case of custom endpoint please add 'host' property here with url 15 | }) 16 | ); 17 | } 18 | return posthog.client; 19 | }; 20 | -------------------------------------------------------------------------------- /app/utils/testUtils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RecoilRoot } from 'recoil'; 3 | import { I18nextProvider } from 'react-i18next'; 4 | import { render } from '@testing-library/react-native'; 5 | import i18n from '@app/utils/i18nextTestUtils'; 6 | 7 | export const apiResponseGenerator = (ok, data) => ({ 8 | ok, 9 | data 10 | }); 11 | 12 | export const renderWithI18next = (children, renderFunction = render) => 13 | renderFunction( 14 | 15 | {children} 16 | 17 | ); 18 | 19 | export const renderProvider = children => 20 | render( 21 | 22 | {children} 23 | 24 | ); 25 | export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms)); 26 | -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configure Babel for Expo project with module resolution aliases. 3 | * This function sets up Babel presets and plugins, including module resolution with aliases. 4 | * @param {object} api - The Babel API object (optional, used for caching). 5 | * @param {Function} api.cache - Function used for caching Babel configuration. 6 | * @returns {object} Babel configuration object with presets and plugins. 7 | */ 8 | // eslint-disable-next-line fp/no-mutation 9 | module.exports = function(api = { cache: () => {} }) { 10 | api.cache(true); 11 | return { 12 | presets: ['babel-preset-expo'], 13 | plugins: [ 14 | [ 15 | 'module-resolver', 16 | { 17 | cwd: 'babelrc', 18 | root: ['./src'], 19 | extensions: ['.js', '.ios.js', '.android.js'], 20 | alias: { 21 | '@app': './app', 22 | '@assets': './app/assets', 23 | '@components': './app/components', 24 | '@atoms': './app/components/atoms', 25 | '@molecules': './app/components/molecules', 26 | '@organisms': './app/components/organisms', 27 | '@config': './app/config', 28 | '@navigators': './app/navigators', 29 | '@scenes': './app/scenes', 30 | '@services': './app/services', 31 | '@themes': './app/themes', 32 | '@utils': './app/utils' 33 | } 34 | } 35 | ], 36 | [ 37 | 'module:react-native-dotenv', 38 | { 39 | moduleName: '@env', 40 | path: '.env', 41 | blacklist: null, 42 | whitelist: null, 43 | safe: false, 44 | allowUndefined: true 45 | } 46 | ] 47 | ] 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 3.8.0" 4 | }, 5 | "build": { 6 | "development": { 7 | "developmentClient": true, 8 | "distribution": "internal", 9 | "ios": { 10 | "simulator": true, 11 | "resourceClass": "m-medium" 12 | } 13 | }, 14 | "preview": { 15 | "distribution": "internal", 16 | "ios": { 17 | "resourceClass": "m-medium" 18 | } 19 | }, 20 | "production": { 21 | "ios": { 22 | "resourceClass": "m-medium" 23 | } 24 | } 25 | }, 26 | "submit": { 27 | "production": {} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from 'expo'; 2 | 3 | import App from './App'; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' 8 | ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] 9 | 10 | platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4' 11 | install! 'cocoapods', 12 | :deterministic_uuids => false 13 | 14 | prepare_react_native_project! 15 | 16 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 17 | # because `react-native-flipper` depends on (FlipperKit,...), which will be excluded. To fix this, 18 | # you can also exclude `react-native-flipper` in `react-native.config.js` 19 | # 20 | # ```js 21 | # module.exports = { 22 | # dependencies: { 23 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 24 | # } 25 | # } 26 | # ``` 27 | flipper_config = FlipperConfiguration.disabled 28 | if ENV['NO_FLIPPER'] == '1' then 29 | # Explicitly disabled through environment variables 30 | flipper_config = FlipperConfiguration.disabled 31 | elsif podfile_properties.key?('ios.flipper') then 32 | # Configure Flipper in Podfile.properties.json 33 | if podfile_properties['ios.flipper'] == 'true' then 34 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"]) 35 | elsif podfile_properties['ios.flipper'] != 'false' then 36 | flipper_config = FlipperConfiguration.enabled(["Debug", "Release"], { 'Flipper' => podfile_properties['ios.flipper'] }) 37 | end 38 | end 39 | 40 | target 'reactnativetemplatews' do 41 | use_expo_modules! 42 | config = use_native_modules! 43 | 44 | use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] 45 | use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] 46 | 47 | use_react_native!( 48 | :path => config[:reactNativePath], 49 | :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', 50 | # An absolute path to your application root. 51 | :app_path => "#{Pod::Config.instance.installation_root}/..", 52 | # Note that if you have use_frameworks! enabled, Flipper will not work if enabled 53 | :flipper_configuration => flipper_config 54 | ) 55 | 56 | post_install do |installer| 57 | react_native_post_install( 58 | installer, 59 | config[:reactNativePath], 60 | :mac_catalyst_enabled => false 61 | ) 62 | 63 | # This is necessary for Xcode 14, because it signs resource bundles by default 64 | # when building for devices. 65 | installer.target_installation_results.pod_target_installation_results 66 | .each do |pod_name, target_installation_result| 67 | target_installation_result.resource_bundle_targets.each do |resource_bundle_target| 68 | resource_bundle_target.build_configurations.each do |config| 69 | config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' 70 | end 71 | end 72 | end 73 | end 74 | 75 | post_integrate do |installer| 76 | begin 77 | expo_patch_react_imports!(installer) 78 | rescue => e 79 | Pod::UI.warn e 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /ios/Podfile.properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo.jsEngine": "hermes", 3 | "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true" 4 | } 5 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews.xcodeproj/xcshareddata/xcschemes/reactnativetemplatews.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface AppDelegate : EXAppDelegateWrapper 6 | 7 | @end 8 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 9 | { 10 | self.moduleName = @"main"; 11 | 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = @{}; 15 | 16 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 17 | } 18 | 19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 20 | { 21 | return [self getBundleURL]; 22 | } 23 | 24 | - (NSURL *)getBundleURL 25 | { 26 | #if DEBUG 27 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; 28 | #else 29 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 30 | #endif 31 | } 32 | 33 | // Linking API 34 | - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { 35 | return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; 36 | } 37 | 38 | // Universal Links 39 | - (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { 40 | BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 41 | return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; 42 | } 43 | 44 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 45 | - (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken 46 | { 47 | return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; 48 | } 49 | 50 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 51 | - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error 52 | { 53 | return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; 54 | } 55 | 56 | // Explicitly define remote notification delegates to ensure compatibility with some third-party libraries 57 | - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler 58 | { 59 | return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/ios/reactnativetemplatews/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "App-Icon-1024x1024@1x.png", 5 | "idiom": "universal", 6 | "platform": "ios", 7 | "size": "1024x1024" 8 | } 9 | ], 10 | "info": { 11 | "version": 1, 12 | "author": "expo" 13 | } 14 | } -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "expo" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/SplashScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/SplashScreen.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/ios/reactnativetemplatews/Images.xcassets/SplashScreen.imageset/image.png -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/SplashScreenBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "universal", 5 | "filename": "image.png", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "version": 1, 19 | "author": "expo" 20 | } 21 | } -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Images.xcassets/SplashScreenBackground.imageset/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wednesday-solutions/react-native-template/2d7a3009db5717ac5d346fe13dd268fb990f2d42/ios/reactnativetemplatews/Images.xcassets/SplashScreenBackground.imageset/image.png -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | react-native-template-ws 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 21 | CFBundleShortVersionString 22 | 1.0.0 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleURLSchemes 29 | 30 | com.wednesdaysolutions.rntws 31 | 32 | 33 | 34 | CFBundleVersion 35 | 1 36 | LSRequiresIPhoneOS 37 | 38 | NSAppTransportSecurity 39 | 40 | NSAllowsArbitraryLoads 41 | 42 | NSAllowsLocalNetworking 43 | 44 | 45 | NSCameraUsageDescription 46 | Allow $(PRODUCT_NAME) to access your camera 47 | NSMicrophoneUsageDescription 48 | Allow $(PRODUCT_NAME) to access your microphone 49 | NSPhotoLibraryUsageDescription 50 | Allow $(PRODUCT_NAME) to access your photos 51 | UILaunchStoryboardName 52 | SplashScreen 53 | UIRequiredDeviceCapabilities 54 | 55 | armv7 56 | 57 | UIRequiresFullScreen 58 | 59 | UIStatusBarStyle 60 | UIStatusBarStyleDefault 61 | UISupportedInterfaceOrientations 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationPortraitUpsideDown 65 | 66 | UISupportedInterfaceOrientations~ipad 67 | 68 | UIInterfaceOrientationPortrait 69 | UIInterfaceOrientationPortraitUpsideDown 70 | UIInterfaceOrientationLandscapeLeft 71 | UIInterfaceOrientationLandscapeRight 72 | 73 | UIUserInterfaceStyle 74 | Light 75 | UIViewControllerBasedStatusBarAppearance 76 | 77 | 78 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/SplashScreen.storyboard: -------------------------------------------------------------------------------- 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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/Supporting/Expo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | EXUpdatesCheckOnLaunch 6 | ALWAYS 7 | EXUpdatesEnabled 8 | 9 | EXUpdatesLaunchWaitMs 10 | 0 11 | EXUpdatesSDKVersion 12 | 50.0.0 13 | 14 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char * argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/noop-file.swift: -------------------------------------------------------------------------------- 1 | // 2 | // @generated 3 | // A blank Swift file must be created for native modules with Swift files to work correctly. 4 | // 5 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/reactnativetemplatews-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 | -------------------------------------------------------------------------------- /ios/reactnativetemplatews/reactnativetemplatews.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | -------------------------------------------------------------------------------- /ios/sentry.properties: -------------------------------------------------------------------------------- 1 | 2 | auth.token=YOUR_SENTRY_AUTH_TOKEN 3 | 4 | defaults.org=YOUR_ORG_NAME 5 | defaults.project=YOUR_PROJECT_NAME 6 | 7 | defaults.url=https://sentry.io/ 8 | -------------------------------------------------------------------------------- /jestconfig.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | collectCoverageFrom: [ 4 | 'app/**/*.{js,jsx}', 5 | '!app/**/*.test.{js,jsx}', 6 | '!app/*/RbGenerated*/*.{js,jsx}', 7 | '!app/app.js', 8 | '!app/global-styles.js', 9 | '!app/*/*/Loadable.{js,jsx}', 10 | '!**/coverage/**' 11 | ], 12 | reporters: [ 13 | 'default', 14 | [ 15 | 'jest-sonar', 16 | { 17 | outputDirectory: 'reports', 18 | outputName: 'test-report.xml', 19 | relativeRootDir: './', 20 | reportedFilePath: 'relative' 21 | } 22 | ] 23 | ], 24 | coverageThreshold: { 25 | global: { 26 | statements: 50, 27 | branches: 50, 28 | functions: 50, 29 | lines: 50 30 | } 31 | }, 32 | preset: 'react-native', 33 | moduleDirectories: ['node_modules', 'app'], 34 | moduleNameMapper: { 35 | '@app(.*)$': '/app/$1', 36 | '@(atoms|molecules|organisms)(.*)$': '/app/components/$1/$2', 37 | '@(containers|components|services|utils|themes|scenes|navigators)(.*)$': 38 | '/app/$1/$2' 39 | }, 40 | setupFiles: ['./node_modules/react-native-gesture-handler/jestSetup.js'], 41 | setupFilesAfterEnv: ['./setupTests.js'], 42 | transformIgnorePatterns: ['/node_modules/(?!react-native)/.+'] 43 | }; 44 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@app/*": ["app/*"], 6 | "@scenes/*": ["app/scenes/*"], 7 | "@components/*": ["app/components/*"], 8 | "@utils/*": ["app/utils/*"], 9 | "@assets/*": ["app/assets/*"], 10 | "@atoms/*": ["app/components/atoms/*"], 11 | "@molecules/*": ["app/components/molecules/*"], 12 | "@organisms/*": ["app/components/organisms/*"], 13 | "@config/*": ["app/config/*"], 14 | "@navigators/*": ["app/navigators/*"], 15 | "@services/*": ["app/services/*"], 16 | "@themes/*": ["app/themes/*"] 17 | } 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getSentryExpoConfig } = require('@sentry/react-native/metro'); 3 | 4 | // eslint-disable-next-line fp/no-mutation 5 | module.exports = getSentryExpoConfig(__dirname); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "expo start --dev-client", 4 | "android": "expo run:android", 5 | "ios": "expo run:ios", 6 | "web": "expo start --web", 7 | "eject": "expo eject", 8 | "test": "jest --detectOpenHandles --coverage", 9 | "test:staged": "jest --findRelatedTests ", 10 | "lint": "npm run lint:js", 11 | "lint:eslint": "eslint --ignore-path .eslintignore", 12 | "lint:eslint:fix": "eslint --ignore-path .eslintignore --fix", 13 | "lint:js": "npm run lint:eslint -- . ", 14 | "lint:staged": "lint-staged", 15 | "rename": "react-native-rename", 16 | "precommit": "lint-staged", 17 | "prettify": "prettier --write", 18 | "initialize": "git checkout --orphan temp-branch && git add -A && git commit -m 'Initial commit' && git branch -D master && git branch -m master", 19 | "build-android": "cd android && ./gradlew assembleRelease" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "pre-commit": "lint-staged" 24 | } 25 | }, 26 | "pre-commit": "lint:staged", 27 | "lint-staged": { 28 | "*.js": [ 29 | "npm run lint:eslint:fix", 30 | "git add --force", 31 | "jest --findRelatedTests $STAGED_FILES" 32 | ], 33 | "*.json": [ 34 | "prettier --write", 35 | "git add --force" 36 | ] 37 | }, 38 | "dependencies": { 39 | "@babel/polyfill": "7.12.1", 40 | "@expo/webpack-config": "~19.0.1", 41 | "@growthbook/growthbook": "^1.2.0", 42 | "@react-native-async-storage/async-storage": "1.23.1", 43 | "@react-native-community/masked-view": "^0.1.11", 44 | "@react-native/metro-config": "^0.75.0-main", 45 | "@react-navigation/compat": "^5.3.15", 46 | "@react-navigation/native": "^6.1.17", 47 | "@react-navigation/stack": "^6.3.29", 48 | "@sentry/react-native": "^5.30.0", 49 | "axios": "^0.27.2", 50 | "axios-mock-adapter": "^1.17.0", 51 | "babel-plugin-module-resolver": "^5.0.0", 52 | "babel-plugin-react-intl": "8.2.25", 53 | "babel-preset-react-native": "^4.0.1", 54 | "eslint-plugin-fp": "^2.3.0", 55 | "eslint-plugin-github": "^4.10.2", 56 | "eslint-plugin-immutable": "^1.0.0", 57 | "eslint-plugin-security": "^3.0.0", 58 | "eslint-plugin-sonarjs": "^0.25.1", 59 | "expo": "^50.0.17", 60 | "expo-crypto": "^13.0.2", 61 | "expo-image-picker": "~14.7.1", 62 | "expo-splash-screen": "~0.26.4", 63 | "expo-status-bar": "~1.11.1", 64 | "global": "^4.4.0", 65 | "i18next": "^23.14.0", 66 | "immer": "^4.0.0", 67 | "immutable": "^5.0.0-beta.5", 68 | "intl": "1.2.5", 69 | "lodash": "^4.17.15", 70 | "map-keys-deep": "^0.0.2", 71 | "posthog-react-native": "^3.1.2", 72 | "prop-types": "^15.7.2", 73 | "react": "18.2.0", 74 | "react-dom": "18.2.0", 75 | "react-i18next": "^15.0.1", 76 | "react-intl": "2.8.0", 77 | "react-native": "0.73.6", 78 | "react-native-cli": "^2.0.1", 79 | "react-native-device-info": "^11.1.0", 80 | "react-native-gesture-handler": "~2.16.0", 81 | "react-native-reanimated": "^3.7.1", 82 | "react-native-safe-area-context": "^4.9.0", 83 | "react-native-screens": "~3.30.1", 84 | "react-native-web": "~0.19.6", 85 | "react-redux": "^9.1.1", 86 | "react-router-dom": "^6.22.3", 87 | "recoil": "^0.7.7", 88 | "reselect": "^5.1.0", 89 | "styled-components": "~5.3.0" 90 | }, 91 | "resolutions": { 92 | "styled-components": "^5" 93 | }, 94 | "devDependencies": { 95 | "@babel/cli": "7.24.1", 96 | "@babel/core": "^7.20.0", 97 | "@babel/plugin-proposal-class-properties": "7.18.6", 98 | "@babel/plugin-syntax-dynamic-import": "7.8.3", 99 | "@babel/plugin-transform-modules-commonjs": "7.24.1", 100 | "@babel/plugin-transform-react-constant-elements": "7.24.1", 101 | "@babel/plugin-transform-react-inline-elements": "7.24.1", 102 | "@babel/preset-env": "7.24.4", 103 | "@babel/preset-react": "7.24.1", 104 | "@babel/register": "7.23.7", 105 | "@babel/runtime": "^7.5.5", 106 | "@react-native-community/eslint-config": "^3.2.0", 107 | "@testing-library/react-native": "^12.4.5", 108 | "babel-core": "7.0.0-bridge.0", 109 | "babel-eslint": "10.0.1", 110 | "babel-jest": "^29.7.0", 111 | "babel-loader": "9.1.3", 112 | "babel-plugin-dynamic-import-node": "2.3.3", 113 | "babel-plugin-lodash": "3.3.4", 114 | "babel-plugin-react-intl": "8.2.25", 115 | "babel-plugin-styled-components": "2.1.4", 116 | "babel-plugin-transform-react-remove-prop-types": "0.4.24", 117 | "babel-preset-expo": "^10.0.0", 118 | "eslint": "^6.1.0", 119 | "eslint-config-airbnb": "^18.0.1", 120 | "eslint-config-prettier": "^4.1.0", 121 | "eslint-config-standard": "^12.0.0", 122 | "eslint-import-resolver-babel-module": "^5.1.0", 123 | "eslint-import-resolver-node": "^0.3.2", 124 | "eslint-import-resolver-reactnative": "^1.0.2", 125 | "eslint-plugin-import": "^2.16.0", 126 | "eslint-plugin-jest": "^22.5.1", 127 | "eslint-plugin-jsx-a11y": "^6.1.1", 128 | "eslint-plugin-node": "^8.0.1", 129 | "eslint-plugin-prettier": "^3.0.1", 130 | "eslint-plugin-promise": "^4.0.1", 131 | "eslint-plugin-react": "^7.12.4", 132 | "eslint-plugin-react-native": "^3.6.0", 133 | "eslint-plugin-redux-saga": "^1.1.1", 134 | "eslint-plugin-standard": "^4.0.0", 135 | "husky": "^9.0.11", 136 | "jest": "^29.7.0", 137 | "jest-sonar": "^0.2.16", 138 | "pre-commit": "1.2.2", 139 | "prettier": "^1.18.2", 140 | "prettier-eslint": "^9.0.0", 141 | "prettier-standard": "^15.0.1", 142 | "pretty-quick": "^1.11.1", 143 | "react-native-dotenv": "^3.4.11", 144 | "react-native-rename": "^3.2.14", 145 | "react-test-renderer": "^18.2.0" 146 | }, 147 | "jest": { 148 | "preset": "react-native", 149 | "setupFiles": [ 150 | "./node_modules/react-native-gesture-handler/jestSetup.js" 151 | ], 152 | "setupFilesAfterEnv": [ 153 | "./setupTests.js" 154 | ], 155 | "transformIgnorePatterns": [ 156 | "/node_modules/(?!react-native)/.+" 157 | ] 158 | }, 159 | "private": true, 160 | "name": "react-native-template", 161 | "version": "1.0.0" 162 | } 163 | -------------------------------------------------------------------------------- /react_native_template_github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'; 2 | import { LogBox } from 'react-native'; 3 | jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage); 4 | 5 | jest.mock('i18next', () => { 6 | const originalI18next = jest.requireActual('i18next'); 7 | 8 | return { 9 | ...originalI18next, 10 | use: jest.fn().mockReturnThis(), 11 | init: jest.fn().mockImplementation((options, callback) => { 12 | if (callback) callback(); 13 | return Promise.resolve(); 14 | }), 15 | t: jest.fn().mockImplementation(key => key), // Simple mock implementation for translations 16 | language: 'en' 17 | }; 18 | }); 19 | 20 | jest.mock('react-i18next', () => ({ 21 | ...jest.requireActual('react-i18next'), 22 | initReactI18next: { 23 | type: '3rdParty', 24 | init: jest.fn() 25 | }, 26 | useTranslation: () => ({ 27 | t: jest.fn().mockImplementation(key => key), 28 | i18n: { 29 | changeLanguage: jest.fn().mockResolvedValue('en'), 30 | language: 'en' 31 | } 32 | }) 33 | })); 34 | 35 | LogBox.ignoreAllLogs(); 36 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=wednesday-solutions_react-native-template_AY7hdnRSB2n8RRmGoU2M 2 | sonar.language=js 3 | sonar.sources=. 4 | sonar.tests=app 5 | sonar.exclusions=**/android/**,**/ios/**,**/tests/**/*.*,jest.config.js,setupTests.js,babel.config.js,metro.config.js 6 | sonar.test.inclusions=**/*.test.js 7 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 8 | sonar.testExecutionReportPaths=reports/test-report.xml 9 | sonar.sourceEncoding=UTF-8 -------------------------------------------------------------------------------- /web-build/register-service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | if ('serviceWorker' in navigator) { 4 | window.addEventListener('load', function() { 5 | navigator.serviceWorker 6 | .register('/expo-service-worker.js', { scope: '/' }) 7 | .then(function(info) { 8 | console.info('Registered service-worker', info); 9 | }) 10 | .catch(function(error) { 11 | console.info('Failed to register service-worker', error); 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const createExpoWebpackConfigAsync = require('@expo/webpack-config'); 2 | 3 | module.exports = async function(env, argv) { 4 | const config = await createExpoWebpackConfigAsync(env, argv); 5 | config.resolve.fallback = { 6 | ...config.resolve.fallback, 7 | crypto: require.resolve('expo-crypto') 8 | }; 9 | return config; 10 | }; 11 | --------------------------------------------------------------------------------