├── .eslintignore ├── .eslintrc ├── .github ├── adding-navigation.gif ├── basic-tabs.gif ├── store-with-todos.png ├── todo-mvc-first-run.png ├── todo-tabs.png ├── todomvc-initial.png ├── todomvc.png └── todosmvc.gif ├── .gitignore ├── LICENSE ├── README.MD ├── app ├── .babelrc ├── .buckconfig ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── android │ ├── app │ │ ├── BUCK │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── todos │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── keystores │ │ ├── BUCK │ │ └── debug.keystore.properties │ └── settings.gradle ├── fastlane │ ├── Fastfile │ ├── Gemfile │ ├── Matchfile │ └── README.md ├── images │ ├── active@2x.png │ ├── all@2x.png │ └── completed@2x.png ├── index.android.js ├── index.ios.js ├── ios │ ├── Todos.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Todos.xcscheme │ ├── Todos │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Base.lproj │ │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ └── Icon-App-60x60@3x.png │ │ │ ├── Brand Assets.launchimage │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ └── LaunchImage.launchimage │ │ │ │ ├── Contents.json │ │ │ │ ├── Default-568h@2x-1.png │ │ │ │ ├── Default-568h@2x.png │ │ │ │ ├── Default-667h@2x.png │ │ │ │ ├── Default-Landscape-736h@3x.png │ │ │ │ ├── Default-Portrait-736h@3x.png │ │ │ │ ├── Default@2x-1.png │ │ │ │ ├── Default@2x.png │ │ │ │ └── todoslaunch56.png │ │ ├── Info.plist │ │ └── main.m │ └── TodosTests │ │ ├── Info.plist │ │ └── TodosTests.m ├── package.json ├── settings │ ├── development │ │ ├── android.json │ │ ├── base.json │ │ ├── ios.json │ │ └── server.json │ └── production │ │ ├── .gitignore │ │ ├── android.json │ │ ├── base.json │ │ └── ios.json ├── src │ ├── api │ │ └── auth.js │ ├── components │ │ ├── AddTodoItem │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── App │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── MainNavigation │ │ │ ├── helpers.js │ │ │ ├── images │ │ │ │ └── hamburger@2x.png │ │ │ ├── index.android.js │ │ │ ├── index.ios.js │ │ │ └── styles.js │ │ ├── NoTodos │ │ │ ├── index.js │ │ │ └── styles.js │ │ ├── TodoItem │ │ │ ├── images │ │ │ │ ├── checked@2x.png │ │ │ │ ├── error@2x.png │ │ │ │ └── unchecked@2x.png │ │ │ ├── index.js │ │ │ └── styles.js │ │ └── TodoList │ │ │ ├── index.js │ │ │ └── styles.js │ ├── sagas │ │ └── index.js │ ├── settings.js │ ├── setup.js │ ├── state │ │ ├── action-types.js │ │ ├── apollo.js │ │ ├── index.js │ │ ├── navigation │ │ │ ├── reducer.js │ │ │ └── selectors.js │ │ └── todos │ │ │ ├── fragments.js │ │ │ ├── helpers.js │ │ │ ├── mutations.js │ │ │ └── queries.js │ └── store.js └── yarn.lock ├── baker ├── ci │ └── run-tests.js ├── generate.js └── generators │ ├── .eslintrc │ ├── app │ ├── index.js │ └── templates │ │ ├── app │ │ ├── reducers.js │ │ ├── sagas │ │ │ └── index.js │ │ ├── setup.js │ │ ├── store.js │ │ └── tests.js │ │ ├── index.js.hbs │ │ ├── server │ │ ├── graphql │ │ │ ├── index.js │ │ │ └── schema.js │ │ ├── index.js │ │ ├── parse-server │ │ │ └── index.js │ │ └── public │ │ │ └── images │ │ │ └── logo.png │ │ ├── settings.js │ │ ├── settings │ │ ├── development.android.json │ │ ├── development.ios.json │ │ └── development.json │ │ └── setup-rn.js │ ├── base.js │ ├── boilerplates.js │ ├── component │ ├── index.js │ └── templates │ │ ├── boilerplates │ │ ├── Vanila.js.hbs │ │ └── navigation │ │ │ ├── Cards.js.hbs │ │ │ ├── Tabs.android.js.hbs │ │ │ └── Tabs.ios.js.hbs │ │ ├── index.js.hbs │ │ ├── index.test.js.hbs │ │ ├── partials │ │ └── mapDispatchPropsAndConnect.js.hbs │ │ └── styles.js.hbs │ ├── container │ └── index.js │ ├── escodegen.js │ ├── esprima.js │ ├── list │ └── index.js │ ├── model │ ├── index.js │ └── templates │ │ ├── schema.js.hbs │ │ └── server │ │ └── models │ │ └── index.js.hbs │ ├── naming.js │ ├── navigation │ └── index.js │ ├── reducer │ ├── index.js │ └── templates │ │ ├── actions.js.hbs │ │ ├── actions.test.js.hbs │ │ ├── boilerplates │ │ ├── Vanila.js.hbs │ │ └── navigation │ │ │ ├── Cards.js.hbs │ │ │ └── Tabs.js.hbs │ │ ├── constants.js.hbs │ │ ├── reducer.js.hbs │ │ ├── reducer.test.js.hbs │ │ └── reducers.js.hbs │ ├── saga │ ├── index.js │ └── templates │ │ ├── boilerplates │ │ ├── MethodCall.js.hbs │ │ └── Vanila.js.hbs │ │ ├── index.js │ │ └── saga.js.hbs │ └── test │ ├── .eslintrc │ ├── setup.js │ └── tests │ ├── app.js │ ├── base.js │ ├── component.js │ ├── container.js │ ├── fixtures │ ├── package.json │ ├── random-file.txt │ ├── reducers.js.template │ └── sagas.index.js.template │ ├── model.js │ ├── navigation.js │ ├── reducer.js │ └── saga.js ├── gradle-bump ├── package.json ├── server ├── Procfile ├── package.json ├── public │ └── images │ │ └── todo-mvc-icon.png ├── scripts │ ├── server-deploy.js │ └── server.js └── src │ ├── api │ └── todo │ │ ├── model.js │ │ ├── resolver.js │ │ └── schema.js │ ├── graphql │ ├── index.js │ └── schema.js │ ├── index.js │ └── parse-server │ └── index.js └── settings /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "parserOptions": { 7 | "ecmaVersion": 6, 8 | "sourceType": "module", 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "rules": { 14 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 15 | "react/prefer-stateless-function": "off" 16 | }, 17 | "globals": { 18 | "__BUNDLE_START_TIME__": false, 19 | "__DEV__": true, 20 | "__dirname": false, 21 | "__filename": false, 22 | "__fbBatchedBridgeConfig": false, 23 | "alert": false, 24 | "cancelAnimationFrame": false, 25 | "clearImmediate": true, 26 | "clearInterval": false, 27 | "clearTimeout": false, 28 | "console": false, 29 | "document": false, 30 | "escape": false, 31 | "exports": false, 32 | "global": false, 33 | "jest": false, 34 | "pit": false, 35 | "Map": true, 36 | "module": false, 37 | "navigator": false, 38 | "process": false, 39 | "Promise": false, 40 | "requestAnimationFrame": true, 41 | "require": false, 42 | "Set": true, 43 | "setImmediate": true, 44 | "setInterval": false, 45 | "setTimeout": false, 46 | "window": false, 47 | "FormData": true, 48 | "XMLHttpRequest": false, 49 | 50 | // Flow "known-globals" annotations: 51 | "ReactElement": false, 52 | "ReactClass": false, 53 | "Class": false, 54 | 55 | // sinon + chai globals for testing 56 | "expect": true, 57 | "mock": true, 58 | "sandbox": true, 59 | "sinon": true, 60 | "spy": true, 61 | "stub": true, 62 | "useFakeServer": true, 63 | "useFakeTimers": true, 64 | "useFakeXMLHttpRequest": true 65 | }, 66 | "env": { 67 | "browser": true, 68 | "node": true, 69 | "mocha": true 70 | } 71 | } -------------------------------------------------------------------------------- /.github/adding-navigation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/adding-navigation.gif -------------------------------------------------------------------------------- /.github/basic-tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/basic-tabs.gif -------------------------------------------------------------------------------- /.github/store-with-todos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/store-with-todos.png -------------------------------------------------------------------------------- /.github/todo-mvc-first-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/todo-mvc-first-run.png -------------------------------------------------------------------------------- /.github/todo-tabs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/todo-tabs.png -------------------------------------------------------------------------------- /.github/todomvc-initial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/todomvc-initial.png -------------------------------------------------------------------------------- /.github/todomvc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/todomvc.png -------------------------------------------------------------------------------- /.github/todosmvc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/.github/todosmvc.gif -------------------------------------------------------------------------------- /.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 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | android/keystores/debug.keystore 42 | 43 | # Ruby 44 | Gemfile.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 The Bakery (http://thebakery.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ⚠️ This app has been migrated away from the original stack described in the [article](http://blog.thebakery.io/todomvc-with-react-native-and-redux/) to using [Apollo Stack](http://www.apollodata.com/) for data management. Followup article coming up ⚠️ 2 | 3 | # TodoMVC: React Native + Parse Server + Apollo Stack 4 | 5 | [TodoMVC](http://todomvc.com/) using React Native with Parse Server persistence layer 6 | 7 | [![Build Status](https://www.bitrise.io/app/8cf4360e8c7dc8b3.svg?token=Lh3j-hKFhrtFiD_Al3pOiA&branch=master)](https://www.bitrise.io/app/8cf4360e8c7dc8b3) 8 | 9 | ![React Native TodosMVC](.github/todomvc.png) 10 | 11 | ## Quick start 12 | 13 | 14 | ```sh 15 | git clone https://github.com/thebakeryio/todomvc-react-native.git 16 | cd todomvc-react-native && npm install 17 | ``` 18 | 19 | To run the server 20 | 21 | ```sh 22 | cd server 23 | npm run server 24 | ``` 25 | 26 | To run app on iOS/Android emulator 27 | 28 | ```sh 29 | cd app 30 | npm run ios 31 | npm run android 32 | ``` 33 | 34 | ## What's included 35 | 36 | - React Native based mobile client (iOS + Android) 37 | - Redux + Sagas for state management and sync 38 | - Parse Server + Parse Server Dashboard + GraphQL Instance 39 | 40 | ## Configuration 41 | 42 | All the settings are located in /settings 43 | -------------------------------------------------------------------------------- /app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } -------------------------------------------------------------------------------- /app/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /app/.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 | .*/Libraries/react-native/React.js 15 | .*/Libraries/react-native/ReactNative.js 16 | 17 | [include] 18 | 19 | [libs] 20 | node_modules/react-native/Libraries/react-native/react-native-interface.js 21 | node_modules/react-native/flow 22 | flow/ 23 | 24 | [options] 25 | module.system=haste 26 | 27 | experimental.strict_type_args=true 28 | 29 | munge_underscores=true 30 | 31 | 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' 32 | 33 | suppress_type=$FlowIssue 34 | suppress_type=$FlowFixMe 35 | suppress_type=$FixMe 36 | 37 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-6]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 38 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-6]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 39 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 40 | 41 | unsafe.enable_getters_and_setters=true 42 | 43 | [version] 44 | ^0.36.0 45 | -------------------------------------------------------------------------------- /app/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /app/.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 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | android/app/libs 42 | *.keystore 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://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 50 | 51 | fastlane/report.xml 52 | fastlane/Preview.html 53 | fastlane/screenshots 54 | -------------------------------------------------------------------------------- /app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app/android/app/BUCK: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # To learn about Buck see [Docs](https://buckbuild.com/). 4 | # To run your application with Buck: 5 | # - install Buck 6 | # - `npm start` - to start the packager 7 | # - `cd android` 8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 10 | # - `buck install -r android/app` - compile, install and run application 11 | # 12 | 13 | lib_deps = [] 14 | for jarfile in glob(['libs/*.jar']): 15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) 16 | lib_deps.append(':' + name) 17 | prebuilt_jar( 18 | name = name, 19 | binary_jar = jarfile, 20 | ) 21 | 22 | for aarfile in glob(['libs/*.aar']): 23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) 24 | lib_deps.append(':' + name) 25 | android_prebuilt_aar( 26 | name = name, 27 | aar = aarfile, 28 | ) 29 | 30 | android_library( 31 | name = 'all-libs', 32 | exported_deps = lib_deps 33 | ) 34 | 35 | android_library( 36 | name = 'app-code', 37 | srcs = glob([ 38 | 'src/main/java/**/*.java', 39 | ]), 40 | deps = [ 41 | ':all-libs', 42 | ':build_config', 43 | ':res', 44 | ], 45 | ) 46 | 47 | android_build_config( 48 | name = 'build_config', 49 | package = 'io.thebakery.todomvc', 50 | ) 51 | 52 | android_resource( 53 | name = 'res', 54 | res = 'src/main/res', 55 | package = 'io.thebakery.todomvc', 56 | ) 57 | 58 | android_binary( 59 | name = 'app', 60 | package_type = 'debug', 61 | manifest = 'src/main/AndroidManifest.xml', 62 | keystore = '//android/keystores:debug', 63 | deps = [ 64 | ':app-code', 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /app/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // the root of your project, i.e. where "package.json" lives 37 | * root: "../../", 38 | * 39 | * // where to put the JS bundle asset in debug mode 40 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 41 | * 42 | * // where to put the JS bundle asset in release mode 43 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 44 | * 45 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 46 | * // require('./image.png')), in debug mode 47 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 48 | * 49 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 50 | * // require('./image.png')), in release mode 51 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 52 | * 53 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 54 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 55 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 56 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 57 | * // for example, you might want to remove it from here. 58 | * inputExcludes: ["android/**", "ios/**"], 59 | * 60 | * // override which node gets called and with what additional arguments 61 | * nodeExecutableAndArgs: ["node"] 62 | * 63 | * // supply additional arguments to the packager 64 | * extraPackagerArgs: [] 65 | * ] 66 | */ 67 | 68 | apply from: "../../node_modules/react-native/react.gradle" 69 | 70 | /** 71 | * Set this to true to create two separate APKs instead of one: 72 | * - An APK that only works on ARM devices 73 | * - An APK that only works on x86 devices 74 | * The advantage is the size of the APK is reduced by about 4MB. 75 | * Upload all the APKs to the Play Store and people will download 76 | * the correct one based on the CPU architecture of their device. 77 | */ 78 | def enableSeparateBuildPerCPUArchitecture = false 79 | 80 | /** 81 | * Run Proguard to shrink the Java bytecode in release builds. 82 | */ 83 | def enableProguardInReleaseBuilds = false 84 | 85 | // Create a variable called keystorePropertiesFile, and initialize it to your 86 | // keystore.properties file, in the rootProject folder. 87 | // def keystorePropertiesFile = rootProject.file("keystore.properties") 88 | 89 | // Initialize a new Properties() object called keystoreProperties. 90 | // def keystoreProperties = new Properties() 91 | 92 | // Load your keystore.properties file into the keystoreProperties object. 93 | // keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 94 | 95 | android { 96 | compileSdkVersion 23 97 | buildToolsVersion "23.0.1" 98 | 99 | defaultConfig { 100 | applicationId "io.thebakery.todomvc" 101 | minSdkVersion 16 102 | targetSdkVersion 22 103 | versionCode 7 104 | versionName "1.1" 105 | ndk { 106 | abiFilters "armeabi-v7a", "x86" 107 | } 108 | } 109 | 110 | splits { 111 | abi { 112 | reset() 113 | enable enableSeparateBuildPerCPUArchitecture 114 | universalApk false // If true, also generate a universal APK 115 | include "armeabi-v7a", "x86" 116 | } 117 | } 118 | buildTypes { 119 | release { 120 | minifyEnabled enableProguardInReleaseBuilds 121 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 122 | } 123 | } 124 | // applicationVariants are e.g. debug, release 125 | applicationVariants.all { variant -> 126 | variant.outputs.each { output -> 127 | // For each separate APK per architecture, set a unique version code as described here: 128 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 129 | def versionCodes = ["armeabi-v7a":1, "x86":2] 130 | def abi = output.getFilter(OutputFile.ABI) 131 | if (abi != null) { // null for the universal-debug, universal-release variants 132 | output.versionCodeOverride = 133 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 134 | } 135 | } 136 | } 137 | } 138 | 139 | dependencies { 140 | compile fileTree(dir: "libs", include: ["*.jar"]) 141 | compile "com.android.support:appcompat-v7:23.0.1" 142 | compile "com.facebook.react:react-native:+" // From node_modules 143 | } 144 | 145 | // Run this once to be able to run the application with BUCK 146 | // puts all compile dependencies into folder libs for BUCK to use 147 | task copyDownloadableDepsToLibs(type: Copy) { 148 | from configurations.compile 149 | into 'libs' 150 | } 151 | -------------------------------------------------------------------------------- /app/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # okhttp 54 | 55 | -keepattributes Signature 56 | -keepattributes *Annotation* 57 | -keep class okhttp3.** { *; } 58 | -keep interface okhttp3.** { *; } 59 | -dontwarn okhttp3.** 60 | 61 | # okio 62 | 63 | -keep class sun.misc.Unsafe { *; } 64 | -dontwarn java.nio.file.* 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | -dontwarn okio.** 67 | -------------------------------------------------------------------------------- /app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/todos/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.thebakery.todomvc; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "Todos"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/android/app/src/main/java/com/todos/MainApplication.java: -------------------------------------------------------------------------------- 1 | package io.thebakery.todomvc; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.shell.MainReactPackage; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 18 | @Override 19 | protected boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | return Arrays.asList( 26 | new MainReactPackage() 27 | ); 28 | } 29 | }; 30 | 31 | @Override 32 | public ReactNativeHost getReactNativeHost() { 33 | return mReactNativeHost; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Todos 3 | 4 | -------------------------------------------------------------------------------- /app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.3.1' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | mavenLocal() 18 | jcenter() 19 | maven { 20 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 21 | url "$rootDir/../node_modules/react-native/android" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/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: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 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 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 6 | -------------------------------------------------------------------------------- /app/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /app/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = 'debug', 3 | store = 'debug.keystore', 4 | properties = 'debug.keystore.properties', 5 | visibility = [ 6 | 'PUBLIC', 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /app/android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Todos' 2 | 3 | include ':app' 4 | -------------------------------------------------------------------------------- /app/fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | fastlane_version "1.100.0" 2 | 3 | default_platform :ios 4 | 5 | platform :ios do 6 | desc "Runs all the tests" 7 | lane :test do 8 | scan 9 | end 10 | 11 | desc "Submit a new Beta Build to Apple TestFlight" 12 | desc "This will also make sure the profile is up to date" 13 | lane :beta do 14 | match(type: "appstore", readonly: true) # more information: https://codesigning.guide 15 | 16 | increment_build_number( 17 | xcodeproj: './ios/Todos.xcodeproj' 18 | ) 19 | 20 | ipa = gym( 21 | scheme: "Todos", 22 | project: "./ios/Todos.xcodeproj" 23 | ) 24 | 25 | pilot(ipa: ipa) 26 | 27 | clean_build_artifacts 28 | 29 | commit_version_bump( 30 | message: "👷 Pushed new build on TestFlight (build #{lane_context[SharedValues::BUILD_NUMBER]}) [skip ci]", 31 | xcodeproj: './ios/Todos.xcodeproj' 32 | ) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/fastlane/Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | ruby '>=2.2.3' 4 | 5 | gem 'match' 6 | gem 'gym' 7 | gem 'pilot' 8 | gem 'fastlane' -------------------------------------------------------------------------------- /app/fastlane/Matchfile: -------------------------------------------------------------------------------- 1 | git_url "git+ssh://git@github.com/callmephilip/fastlane-ios-certs.git" 2 | 3 | type "appstore" 4 | 5 | app_identifier "io.thebakery.todos" 6 | username "bakers@thebakery.io" 7 | -------------------------------------------------------------------------------- /app/fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ================ 3 | # Installation 4 | ``` 5 | sudo gem install fastlane 6 | ``` 7 | # Available Actions 8 | ## iOS 9 | ### ios test 10 | ``` 11 | fastlane ios test 12 | ``` 13 | Runs all the tests 14 | ### ios beta 15 | ``` 16 | fastlane ios beta 17 | ``` 18 | Submit a new Beta Build to Apple TestFlight 19 | 20 | This will also make sure the profile is up to date 21 | ### ios appstore 22 | ``` 23 | fastlane ios appstore 24 | ``` 25 | Deploy a new version to the App Store 26 | 27 | ---- 28 | 29 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. 30 | More information about fastlane can be found on [https://fastlane.tools](https://fastlane.tools). 31 | The documentation of fastlane can be found on [GitHub](https://github.com/fastlane/fastlane/tree/master/fastlane). -------------------------------------------------------------------------------- /app/images/active@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/images/active@2x.png -------------------------------------------------------------------------------- /app/images/all@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/images/all@2x.png -------------------------------------------------------------------------------- /app/images/completed@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/images/completed@2x.png -------------------------------------------------------------------------------- /app/index.android.js: -------------------------------------------------------------------------------- 1 | const { AppRegistry } = require('react-native'); 2 | const setup = require('./src/setup'); 3 | 4 | AppRegistry.registerComponent('Todos', setup); 5 | -------------------------------------------------------------------------------- /app/index.ios.js: -------------------------------------------------------------------------------- 1 | const { AppRegistry } = require('react-native'); 2 | const setup = require('./src/setup'); 3 | 4 | AppRegistry.registerComponent('Todos', setup); 5 | -------------------------------------------------------------------------------- /app/ios/Todos.xcodeproj/xcshareddata/xcschemes/Todos.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 61 | 67 | 68 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 113 | 119 | 120 | 121 | 122 | 124 | 125 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /app/ios/Todos/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /app/ios/Todos/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import 13 | #import 14 | 15 | @implementation AppDelegate 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 18 | { 19 | NSURL *jsCodeLocation; 20 | 21 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; 22 | 23 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 24 | moduleName:@"Todos" 25 | initialProperties:nil 26 | launchOptions:launchOptions]; 27 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 28 | 29 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 30 | UIViewController *rootViewController = [UIViewController new]; 31 | rootViewController.view = rootView; 32 | self.window.rootViewController = rootViewController; 33 | [self.window makeKeyAndVisible]; 34 | return YES; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /app/ios/Todos/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "29x29", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-29x29@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "29x29", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-29x29@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "40x40", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-40x40@2x.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "40x40", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-40x40@3x.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "60x60", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-60x60@2x.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "60x60", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-60x60@3x.png", 37 | "scale" : "3x" 38 | } 39 | ], 40 | "info" : { 41 | "version" : 1, 42 | "author" : "xcode" 43 | } 44 | } -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/Brand Assets.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "minimum-system-version" : "7.0", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "orientation" : "portrait", 11 | "idiom" : "iphone", 12 | "minimum-system-version" : "7.0", 13 | "subtype" : "retina4", 14 | "scale" : "2x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "extent" : "full-screen", 5 | "idiom" : "iphone", 6 | "subtype" : "736h", 7 | "filename" : "Default-Portrait-736h@3x.png", 8 | "minimum-system-version" : "8.0", 9 | "orientation" : "portrait", 10 | "scale" : "3x" 11 | }, 12 | { 13 | "extent" : "full-screen", 14 | "idiom" : "iphone", 15 | "subtype" : "736h", 16 | "filename" : "Default-Landscape-736h@3x.png", 17 | "minimum-system-version" : "8.0", 18 | "orientation" : "landscape", 19 | "scale" : "3x" 20 | }, 21 | { 22 | "extent" : "full-screen", 23 | "idiom" : "iphone", 24 | "subtype" : "667h", 25 | "filename" : "Default-667h@2x.png", 26 | "minimum-system-version" : "8.0", 27 | "orientation" : "portrait", 28 | "scale" : "2x" 29 | }, 30 | { 31 | "orientation" : "portrait", 32 | "idiom" : "iphone", 33 | "filename" : "Default@2x.png", 34 | "extent" : "full-screen", 35 | "minimum-system-version" : "7.0", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "extent" : "full-screen", 40 | "idiom" : "iphone", 41 | "subtype" : "retina4", 42 | "filename" : "Default-568h@2x.png", 43 | "minimum-system-version" : "7.0", 44 | "orientation" : "portrait", 45 | "scale" : "2x" 46 | }, 47 | { 48 | "orientation" : "portrait", 49 | "idiom" : "iphone", 50 | "filename" : "todoslaunch56.png", 51 | "extent" : "full-screen", 52 | "scale" : "1x" 53 | }, 54 | { 55 | "orientation" : "portrait", 56 | "idiom" : "iphone", 57 | "filename" : "Default@2x-1.png", 58 | "extent" : "full-screen", 59 | "scale" : "2x" 60 | }, 61 | { 62 | "orientation" : "portrait", 63 | "idiom" : "iphone", 64 | "filename" : "Default-568h@2x-1.png", 65 | "extent" : "full-screen", 66 | "subtype" : "retina4", 67 | "scale" : "2x" 68 | } 69 | ], 70 | "info" : { 71 | "version" : 1, 72 | "author" : "xcode" 73 | } 74 | } -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-568h@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-568h@2x-1.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-568h@2x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-667h@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-667h@2x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-Landscape-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-Landscape-736h@3x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-Portrait-736h@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default-Portrait-736h@3x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default@2x-1.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/Default@2x.png -------------------------------------------------------------------------------- /app/ios/Todos/Images.xcassets/LaunchImage.launchimage/todoslaunch56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/ios/Todos/Images.xcassets/LaunchImage.launchimage/todoslaunch56.png -------------------------------------------------------------------------------- /app/ios/Todos/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 17 23 | ITSAppUsesNonExemptEncryption 24 | 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSTemporaryExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /app/ios/Todos/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/ios/TodosTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 16 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/ios/TodosTests/TodosTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import 14 | #import 15 | 16 | #define TIMEOUT_SECONDS 600 17 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 18 | 19 | @interface TodosTests : XCTestCase 20 | 21 | @end 22 | 23 | @implementation TodosTests 24 | 25 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 26 | { 27 | if (test(view)) { 28 | return YES; 29 | } 30 | for (UIView *subview in [view subviews]) { 31 | if ([self findSubviewInView:subview matching:test]) { 32 | return YES; 33 | } 34 | } 35 | return NO; 36 | } 37 | 38 | - (void)testRendersWelcomeScreen 39 | { 40 | UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 41 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 42 | BOOL foundElement = NO; 43 | 44 | __block NSString *redboxError = nil; 45 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 46 | if (level >= RCTLogLevelError) { 47 | redboxError = message; 48 | } 49 | }); 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | RCTSetLogFunction(RCTDefaultLogFunction); 64 | 65 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 66 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 67 | } 68 | 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todos", 3 | "version": "1.0.0", 4 | "description": "React Native app powered by Baker", 5 | "author": "", 6 | "license": "ISC", 7 | "scripts": { 8 | "ios": "node node_modules/react-native/local-cli/cli.js run-ios", 9 | "android": "node node_modules/react-native/local-cli/cli.js run-android" 10 | }, 11 | "engines": { 12 | "node": ">=4.3" 13 | }, 14 | "dependencies": { 15 | "apollo-client": "^0.5.7", 16 | "graphql-tag": "^1.0.0", 17 | "immutable": "^3.8.1", 18 | "lodash": "^4.17.2", 19 | "parse": "1.9.2", 20 | "react": "~15.4.1", 21 | "react-addons-update": "^15.4.1", 22 | "react-apollo": "^0.6.0", 23 | "react-native": "0.40.0", 24 | "react-native-navigation-redux-helpers": "^0.5.0", 25 | "react-native-swipeout": "git+https://github.com/magrinj/react-native-swipeout.git#403d973504b58ede25c137fb31b0eff7e03b0a66", 26 | "react-redux": "^4.4.5", 27 | "redux": "^3.6.0", 28 | "redux-immutable": "^3.0.8", 29 | "redux-saga": "^0.13.0", 30 | "reselect": "^2.5.4" 31 | }, 32 | "devDependencies": { 33 | "remote-redux-devtools": "^0.5.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/settings/development/android.json: -------------------------------------------------------------------------------- 1 | { 2 | "parseServerURL": "http://10.0.2.2:8000/parse", 3 | "graphqlURL": "http://10.0.2.2:8000/graphql" 4 | } -------------------------------------------------------------------------------- /app/settings/development/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverPort": 8000, 3 | "parseServerApplicationId": "parse-app-id", 4 | "parseServerURL": "http://localhost:8000/parse", 5 | "graphqlURL": "http://localhost:8000/graphql" 6 | } -------------------------------------------------------------------------------- /app/settings/development/ios.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /app/settings/development/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "parseServerMasterKey": "70c6093dba5a7e55968a1c7ad3dd3e5a74ef5cac", 3 | "parseServerDatabaseURI": "mongodb://localhost:27017/dev" 4 | } -------------------------------------------------------------------------------- /app/settings/production/.gitignore: -------------------------------------------------------------------------------- 1 | server.json -------------------------------------------------------------------------------- /app/settings/production/android.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /app/settings/production/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverPort": 80, 3 | "parseServerApplicationId": "parse-app-id", 4 | "parseServerURL": "https://todomvc-rn.herokuapp.com/parse", 5 | "graphqlURL": "https://todomvc-rn.herokuapp.com/graphql" 6 | } -------------------------------------------------------------------------------- /app/settings/production/ios.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /app/src/api/auth.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/react-native'; 2 | 3 | export function getCurrentUser() { 4 | return Parse.User.currentAsync().then((currentUser) => { 5 | if (currentUser) { 6 | return currentUser; 7 | } 8 | 9 | const user = new Parse.User(); 10 | const newEmail = `todos-${Math.floor(Math.random() * 1000000)}@gmail.com`; 11 | user.set('username', newEmail); 12 | user.set('email', newEmail); 13 | user.set('password', 'password'); 14 | 15 | return user.signUp(null); 16 | }).then(user => user, error => error); 17 | } 18 | -------------------------------------------------------------------------------- /app/src/components/AddTodoItem/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * AddTodoItem 4 | * 5 | */ 6 | 7 | import { TextInput } from 'react-native'; 8 | import React, { Component } from 'react'; 9 | import styles from './styles'; 10 | import { withCreateMutation } from '../../state/todos/mutations'; 11 | 12 | class AddTodoItem extends Component { 13 | constructor() { 14 | super(); 15 | 16 | this.saveTodoItem = this.saveTodoItem.bind(this); 17 | } 18 | 19 | saveTodoItem() { 20 | this.props.addTodo({ text: this.state.text }); 21 | this.textBox.clear(); 22 | } 23 | 24 | render() { 25 | return ( 26 | { this.textBox = input; }} 28 | autoCorrect={false} 29 | placeholder={'What needs to be done?'} 30 | style={styles.textBox} 31 | onChangeText={(text) => this.setState({ text })} 32 | onSubmitEditing={this.saveTodoItem} 33 | /> 34 | ); 35 | } 36 | } 37 | 38 | AddTodoItem.propTypes = { 39 | addTodo: React.PropTypes.func.isRequired, 40 | }; 41 | 42 | export default withCreateMutation(AddTodoItem); 43 | -------------------------------------------------------------------------------- /app/src/components/AddTodoItem/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | textBox: { 5 | paddingLeft: 15, 6 | height: 60, 7 | borderWidth: StyleSheet.hairlineWidth, 8 | borderColor: '#ededed', 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /app/src/components/App/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * App 4 | * 5 | */ 6 | 7 | import { View, Platform } from 'react-native'; 8 | import React, { Component } from 'react'; 9 | import styles from './styles'; 10 | // eslint-disable-next-line import/no-unresolved 11 | import MainNavigation from '../MainNavigation'; 12 | import AddTodoItem from '../AddTodoItem'; 13 | 14 | class App extends Component { 15 | render() { 16 | return ( 17 | 18 | {Platform.OS === 'ios' ? : null} 19 | 20 | 21 | ); 22 | } 23 | } 24 | 25 | export default App; 26 | -------------------------------------------------------------------------------- /app/src/components/App/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Platform } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | ...Platform.select({ 7 | ios: { 8 | marginTop: 20, 9 | } 10 | }) 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /app/src/components/MainNavigation/helpers.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AllTodos, CompletedTodos, ActiveTodos } from '../TodoList'; 3 | 4 | export const filterToComponent = { 5 | all: , 6 | active: , 7 | completed: , 8 | }; 9 | -------------------------------------------------------------------------------- /app/src/components/MainNavigation/images/hamburger@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/src/components/MainNavigation/images/hamburger@2x.png -------------------------------------------------------------------------------- /app/src/components/MainNavigation/index.android.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * MainNavigation 4 | * 5 | */ 6 | 7 | import { View, Text, Image, TouchableHighlight } from 'react-native'; 8 | import React, { Component } from 'react'; 9 | import styles from './styles'; 10 | import { connect } from 'react-redux'; 11 | import { createSelector } from 'reselect'; 12 | import { selectMainNavigation } from '../../state/navigation/selectors'; 13 | // eslint-disable-next-line 14 | import DrawerLayoutAndroid from 'DrawerLayoutAndroid'; 15 | // eslint-disable-next-line 16 | import ToolbarAndroid from 'ToolbarAndroid'; 17 | import AddTodoItem from '../AddTodoItem'; 18 | import { actions as navigationActions } from 'react-native-navigation-redux-helpers'; 19 | import { filterToComponent } from './helpers'; 20 | 21 | const { jumpTo } = navigationActions; 22 | 23 | const androidToolbarStyle = { 24 | height: 56, 25 | }; 26 | 27 | // eslint-disable-next-line 28 | const navigationIcon = require('./images/hamburger.png'); 29 | 30 | class MainNavigation extends Component { 31 | constructor() { 32 | super(); 33 | 34 | this.renderNavigation = this.renderNavigation.bind(this); 35 | } 36 | 37 | renderNavigation() { 38 | const onNavigate = (action) => { 39 | this.drawer.closeDrawer(); 40 | this.props.dispatch(action); 41 | }; 42 | return ( 43 | 44 | todos 45 | {this.props.mainNavigation.routes.map((t, i) => ( 46 | onNavigate(jumpTo(i, this.props.mainNavigation.key))} 49 | key={t.key} 50 | > 51 | 52 | 53 | {t.title} 54 | 55 | 56 | ))} 57 | 58 | ); 59 | } 60 | 61 | renderTabContent(tab) { 62 | const todoList = filterToComponent[tab.key]; 63 | return ( 64 | 65 | {todoList} 66 | 67 | ); 68 | } 69 | 70 | renderContent() { 71 | const selectedTab = this.props.mainNavigation.routes[this.props.mainNavigation.index]; 72 | return ( 73 | 74 | this.drawer.openDrawer()} 78 | title={selectedTab.title} 79 | /> 80 | 81 | {this.renderTabContent(selectedTab)} 82 | 83 | ); 84 | } 85 | 86 | render() { 87 | return ( 88 | { this.drawer = drawer; }} 90 | drawerWidth={300} 91 | drawerPosition={DrawerLayoutAndroid.positions.Left} 92 | renderNavigationView={this.renderNavigation} 93 | > 94 | {this.renderContent()} 95 | 96 | ); 97 | } 98 | } 99 | 100 | MainNavigation.propTypes = { 101 | dispatch: React.PropTypes.func.isRequired, 102 | mainNavigation: React.PropTypes.shape({ 103 | index: React.PropTypes.number.isRequired, 104 | key: React.PropTypes.string.isRequired, 105 | routes: React.PropTypes.arrayOf(React.PropTypes.object), 106 | }), 107 | }; 108 | 109 | function mapDispatchToProps(dispatch) { 110 | return { 111 | dispatch, 112 | }; 113 | } 114 | 115 | export default connect( 116 | createSelector(selectMainNavigation, (mainNavigation) => ({ mainNavigation })), 117 | mapDispatchToProps 118 | )(MainNavigation); 119 | -------------------------------------------------------------------------------- /app/src/components/MainNavigation/index.ios.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * MainNavigation 4 | * 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | import styles from './styles'; 9 | import { connect } from 'react-redux'; 10 | import { createSelector } from 'reselect'; 11 | import { selectMainNavigation } from '../../state/navigation/selectors'; 12 | // eslint-disable-next-line 13 | import { TabBarIOS, View } from 'react-native'; 14 | import { actions as navigationActions } from 'react-native-navigation-redux-helpers'; 15 | import { filterToComponent } from './helpers'; 16 | 17 | const { jumpTo } = navigationActions; 18 | 19 | 20 | class MainNavigation extends Component { 21 | renderTabContent(tab) { 22 | const todoList = filterToComponent[tab.key]; 23 | return ( 24 | 25 | {todoList} 26 | 27 | ); 28 | } 29 | 30 | render() { 31 | const { mainNavigation, dispatch } = this.props; 32 | 33 | const children = mainNavigation.routes.map((tab, i) => ( 34 | dispatch(jumpTo(i, mainNavigation.key)) 40 | } 41 | selected={this.props.mainNavigation.index === i} 42 | > 43 | {this.renderTabContent(tab)} 44 | 45 | )); 46 | return ( 47 | 48 | {children} 49 | 50 | ); 51 | } 52 | } 53 | 54 | function mapDispatchToProps(dispatch) { 55 | return { 56 | dispatch, 57 | }; 58 | } 59 | 60 | MainNavigation.propTypes = { 61 | dispatch: React.PropTypes.func.isRequired, 62 | mainNavigation: React.PropTypes.shape({ 63 | index: React.PropTypes.number.isRequired, 64 | key: React.PropTypes.string.isRequired, 65 | routes: React.PropTypes.arrayOf(React.PropTypes.object), 66 | }), 67 | }; 68 | 69 | export default connect( 70 | createSelector(selectMainNavigation, (mainNavigation) => ({ mainNavigation })), 71 | mapDispatchToProps 72 | )(MainNavigation); 73 | -------------------------------------------------------------------------------- /app/src/components/MainNavigation/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | header: { 8 | fontSize: 50, 9 | textAlign: 'center', 10 | color: 'rgba(175, 47, 47, 0.15)', 11 | marginTop: 50, 12 | marginBottom: 100, 13 | }, 14 | navigationMenuItem: { 15 | borderBottomWidth: StyleSheet.hairlineWidth, 16 | borderBottomColor: '#ededed', 17 | flexDirection: 'row', 18 | paddingTop: 15, 19 | paddingBottom: 15, 20 | paddingLeft: 15, 21 | }, 22 | label: { 23 | fontSize: 24, 24 | marginLeft: 10, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /app/src/components/NoTodos/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * NoTodos 4 | * 5 | */ 6 | 7 | import { View, Text } from 'react-native'; 8 | import React, { Component } from 'react'; 9 | import styles from './styles'; 10 | 11 | 12 | class NoTodos extends Component { 13 | render() { 14 | return ( 15 | 16 | Nothing to see here. Move along 17 | 18 | ); 19 | } 20 | } 21 | 22 | 23 | export default NoTodos; 24 | -------------------------------------------------------------------------------- /app/src/components/NoTodos/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | flexDirection: 'row', 7 | alignItems: 'center', 8 | justifyContent: 'center', 9 | }, 10 | text: { 11 | textAlign: 'center', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /app/src/components/TodoItem/images/checked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/src/components/TodoItem/images/checked@2x.png -------------------------------------------------------------------------------- /app/src/components/TodoItem/images/error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/src/components/TodoItem/images/error@2x.png -------------------------------------------------------------------------------- /app/src/components/TodoItem/images/unchecked@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/src/components/TodoItem/images/unchecked@2x.png -------------------------------------------------------------------------------- /app/src/components/TodoItem/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TodoItem 4 | * 5 | */ 6 | 7 | import { View, Text, TouchableOpacity, Image } from 'react-native'; 8 | import React, { Component, PropTypes } from 'react'; 9 | import styles from './styles'; 10 | import Swipeout from 'react-native-swipeout'; 11 | 12 | // eslint-disable-next-line 13 | const checkedIcon = require('./images/checked.png'); 14 | // eslint-disable-next-line 15 | const uncheckedIcon = require('./images/unchecked.png'); 16 | // eslint-disable-next-line 17 | const errorIcon = require('./images/error.png'); 18 | 19 | 20 | class TodoItem extends Component { 21 | renderCheckbox() { 22 | const imageModule = this.props.todo.isComplete ? checkedIcon : uncheckedIcon; 23 | 24 | return ( 25 | 26 | ); 27 | } 28 | 29 | render() { 30 | const swipeoutBtns = [{ 31 | text: 'Delete', 32 | backgroundColor: 'red', 33 | onPress: () => { 34 | const { todo } = this.props; 35 | this.props.onDelete({ todo }); 36 | }, 37 | }]; 38 | const { todo } = this.props; 39 | const viewDisabledStyling = todo.isComplete || todo.isDisabled ? { opacity: 0.5 } : {}; 40 | const labelCompletedStyling = todo.isComplete ? { textDecorationLine: 'line-through' } : {}; 41 | const error = todo.error ? ( 42 | 43 | 44 | {todo.error} 45 | 46 | ) : null; 47 | const item = ( 48 | 49 | {this.renderCheckbox()} 50 | 51 | {todo.text} 52 | {error} 53 | 54 | 55 | ); 56 | 57 | 58 | if (todo.isDisabled) { 59 | return item; 60 | } 61 | 62 | return ( 63 | 64 | this.props.onToggleCompletion({ todo })} 67 | > 68 | {item} 69 | 70 | 71 | ); 72 | } 73 | } 74 | 75 | TodoItem.propTypes = { 76 | todo: PropTypes.object.isRequired, 77 | onToggleCompletion: PropTypes.func.isRequired, 78 | onDelete: PropTypes.func.isRequired, 79 | }; 80 | 81 | export default TodoItem; 82 | -------------------------------------------------------------------------------- /app/src/components/TodoItem/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | item: { 5 | borderBottomWidth: StyleSheet.hairlineWidth, 6 | borderBottomColor: '#ededed', 7 | flexDirection: 'row', 8 | paddingTop: 15, 9 | paddingRight: 60, 10 | }, 11 | labelWrapper: { 12 | flex: 1, 13 | flexDirection: 'column', 14 | alignItems: 'flex-start', 15 | }, 16 | label: { 17 | fontSize: 24, 18 | paddingBottom: 15, 19 | paddingTop: 1, 20 | marginLeft: 10, 21 | }, 22 | checkbox: { 23 | marginLeft: 10, 24 | marginTop: 3, 25 | }, 26 | errorWrapper: { 27 | flexDirection: 'row', 28 | alignSelf: 'stretch', 29 | flex: 1, 30 | justifyContent: 'flex-end', 31 | marginRight: -50, 32 | marginBottom: 10, 33 | }, 34 | errorIcon: { 35 | width: 15, 36 | height: 15, 37 | }, 38 | errorLabel: { 39 | color: 'red', 40 | fontSize: 10, 41 | paddingTop: 1, 42 | marginLeft: 5, 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /app/src/components/TodoList/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * FilteredTodoList 4 | * 5 | */ 6 | 7 | import { View, Text, ListView } from 'react-native'; 8 | import React, { Component, PropTypes } from 'react'; 9 | import styles from './styles'; 10 | import NoTodos from '../NoTodos'; 11 | import TodoItem from '../TodoItem'; 12 | import { withAllTodos, withCompletedTodos, withActiveTodos } from '../../state/todos/queries'; 13 | import { withDeleteMutation, withToggleMutation } from '../../state/todos/mutations'; 14 | 15 | class TodoList extends Component { 16 | constructor() { 17 | super(); 18 | 19 | this.renderRow = this.renderRow.bind(this); 20 | } 21 | 22 | renderList() { 23 | const { todos } = this.props; 24 | if (todos && todos.length !== 0) { 25 | const ds = new ListView.DataSource({ 26 | rowHasChanged: (r1, r2) => r1 !== r2, 27 | }); 28 | 29 | return ( 30 | 34 | ); 35 | } 36 | 37 | return ( 38 | 39 | ); 40 | } 41 | 42 | renderRow(todo) { 43 | return ( 44 | 50 | ); 51 | } 52 | 53 | render() { 54 | const { loading } = this.props; 55 | return ( 56 | 57 | {loading ? Loading... : this.renderList()} 58 | 59 | ); 60 | } 61 | } 62 | 63 | TodoList.propTypes = { 64 | todos: PropTypes.array, 65 | loading: PropTypes.bool.isRequired, 66 | deleteTodo: PropTypes.func.isRequired, 67 | toggleTodoCompletion: PropTypes.func.isRequired, 68 | }; 69 | 70 | export const AllTodos = withDeleteMutation(withToggleMutation(withAllTodos(TodoList))); 71 | export const CompletedTodos = withDeleteMutation(withToggleMutation(withCompletedTodos(TodoList))); 72 | export const ActiveTodos = withDeleteMutation(withToggleMutation(withActiveTodos(TodoList))); 73 | -------------------------------------------------------------------------------- /app/src/components/TodoList/styles.js: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /app/src/sagas/index.js: -------------------------------------------------------------------------------- 1 | export default []; 2 | -------------------------------------------------------------------------------- /app/src/settings.js: -------------------------------------------------------------------------------- 1 | import devBase from '../settings/development/base'; 2 | import deviOS from '../settings/development/ios'; 3 | import devAndroid from '../settings/development/android'; 4 | 5 | import productionBase from '../settings/production/base'; 6 | import productioniOS from '../settings/production/ios'; 7 | import productionAndroid from '../settings/production/android'; 8 | 9 | import { Platform } from 'react-native'; 10 | 11 | export default { 12 | load() { 13 | if (__DEV__) { 14 | return Object.assign({}, devBase, Platform.select({ 15 | ios: deviOS, 16 | android: devAndroid, 17 | })); 18 | } 19 | 20 | return Object.assign({}, productionBase, Platform.select({ 21 | ios: productioniOS, 22 | android: productionAndroid, 23 | })); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /app/src/setup.js: -------------------------------------------------------------------------------- 1 | import App from './components/App'; 2 | import React, { Component } from 'react'; 3 | import configureStore from './store'; 4 | import sagas from './sagas'; 5 | import Parse from 'parse/react-native'; 6 | import Settings from './settings'; 7 | import { ApolloProvider } from 'react-apollo'; 8 | import apollo from './state/apollo'; 9 | 10 | const settings = Settings.load(); 11 | 12 | Parse.initialize(settings.parseServerApplicationId); 13 | Parse.serverURL = settings.parseServerURL; 14 | 15 | const store = configureStore(); 16 | sagas.forEach(saga => store.runSaga(saga)); 17 | 18 | function setup() { 19 | class Root extends Component { 20 | render() { 21 | return ( 22 | 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | return Root; 30 | } 31 | 32 | module.exports = setup; 33 | -------------------------------------------------------------------------------- /app/src/state/action-types.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/app/src/state/action-types.js -------------------------------------------------------------------------------- /app/src/state/apollo.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: off, no-underscore-dangle: off */ 2 | 3 | import ApolloClient, { createNetworkInterface } from 'apollo-client'; 4 | import { getCurrentUser } from '../api/auth'; 5 | import Settings from '../settings'; 6 | 7 | const settings = Settings.load(); 8 | const networkInterface = createNetworkInterface({ uri: settings.graphqlURL }); 9 | 10 | networkInterface.use([{ 11 | applyMiddleware(req, next) { 12 | if (!req.options.headers) { 13 | req.options.headers = {}; // Create the header object if needed. 14 | } 15 | 16 | getCurrentUser().then(user => { 17 | req.options.headers.authorization = user && user.getSessionToken(); 18 | next(); 19 | }, error => { 20 | next(); 21 | }); 22 | }, 23 | }]); 24 | 25 | const client = new ApolloClient({ 26 | networkInterface, 27 | dataIdFromObject: (result) => { 28 | if (result.id && result.__typename) { 29 | return result.__typename + result.id; 30 | } 31 | 32 | // Make sure to return null if this object doesn't have an ID 33 | return null; 34 | }, 35 | }); 36 | 37 | export default client; 38 | -------------------------------------------------------------------------------- /app/src/state/index.js: -------------------------------------------------------------------------------- 1 | import mainNavigation from './navigation/reducer'; 2 | import { combineReducers } from 'redux'; 3 | import apollo from './apollo'; 4 | 5 | const applicationReducers = { 6 | mainNavigation, 7 | apollo: apollo.reducer(), 8 | }; 9 | 10 | export default function createReducer() { 11 | return combineReducers(applicationReducers); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/state/navigation/reducer.js: -------------------------------------------------------------------------------- 1 | /* eslint global-require: off, import/no-unresolved: off */ 2 | 3 | import { Platform } from 'react-native'; 4 | import { tabReducer } from 'react-native-navigation-redux-helpers'; 5 | 6 | const routes = [ 7 | { 8 | key: 'all', 9 | icon: require('../../../images/all.png'), 10 | title: Platform.select({ ios: 'All', android: 'All Tasks' }), 11 | }, 12 | { 13 | key: 'active', 14 | icon: require('../../../images/active.png'), 15 | title: Platform.select({ ios: 'Active', android: 'Active Tasks' }), 16 | }, 17 | { 18 | key: 'completed', 19 | icon: require('../../../images/completed.png'), 20 | title: Platform.select({ ios: 'Completed', android: 'Completed Tasks' }), 21 | }, 22 | ]; 23 | 24 | const mainNavigation = tabReducer({ 25 | key: 'mainNavigation', 26 | routes, 27 | index: 0, 28 | }); 29 | 30 | export default mainNavigation; 31 | -------------------------------------------------------------------------------- /app/src/state/navigation/selectors.js: -------------------------------------------------------------------------------- 1 | export function selectMainNavigation(state) { 2 | return state.mainNavigation; 3 | } 4 | -------------------------------------------------------------------------------- /app/src/state/todos/fragments.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | 3 | export const TodoFields = gql` 4 | fragment TodoFields on Todo { 5 | createdAt 6 | id 7 | isComplete 8 | text 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /app/src/state/todos/helpers.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | export function sortTodos(todos) { 4 | return _.sortBy(todos, todo => -todo.createdAt); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/state/todos/mutations.js: -------------------------------------------------------------------------------- 1 | import { graphql } from 'react-apollo'; 2 | import { getFragmentDefinitions } from 'apollo-client'; 3 | import gql from 'graphql-tag'; 4 | import update from 'react-addons-update'; 5 | import { TodoFields } from './fragments'; 6 | 7 | const toggleCompletionMutation = gql` 8 | mutation toggleTodoCompletion($id: ID!) { 9 | toggleTodoCompletion(id: $id) { 10 | ...TodoFields 11 | } 12 | } 13 | `; 14 | 15 | export const withToggleMutation = graphql(toggleCompletionMutation, { 16 | options: () => ({ 17 | fragments: getFragmentDefinitions(TodoFields), 18 | }), 19 | props: ({ mutate }) => ({ 20 | toggleTodoCompletion: ({ todo }) => mutate({ 21 | variables: { id: todo.id }, 22 | optimisticResponse: { 23 | __typename: 'Mutation', 24 | toggleTodoCompletion: Object.assign({ 25 | __typename: 'Todo', 26 | }, todo, { 27 | isComplete: !todo.isComplete, 28 | }), 29 | }, 30 | }), 31 | }), 32 | }); 33 | 34 | const deleteMutation = gql` 35 | mutation deleteTodo($id: ID!) { 36 | deleteTodo(id: $id) { 37 | ...TodoFields 38 | } 39 | } 40 | `; 41 | 42 | function updateQueryAfterDelete(prev, { mutationResult }) { 43 | const deletedTodo = mutationResult.data.deleteTodo; 44 | return update(prev, { 45 | todos: { 46 | $set: prev.todos.filter(t => t.id !== deletedTodo.id), 47 | }, 48 | }); 49 | } 50 | 51 | export const withDeleteMutation = graphql(deleteMutation, { 52 | options: () => ({ 53 | fragments: getFragmentDefinitions(TodoFields), 54 | }), 55 | props: ({ mutate }) => ({ 56 | deleteTodo: ({ todo }) => mutate({ 57 | variables: { id: todo.id }, 58 | optimisticResponse: { 59 | __typename: 'Mutation', 60 | deleteTodo: Object.assign({ 61 | __typename: 'Todo', 62 | }, todo), 63 | }, 64 | updateQueries: { 65 | allTodos: updateQueryAfterDelete, 66 | activeTodos: updateQueryAfterDelete, 67 | completedTodos: updateQueryAfterDelete, 68 | }, 69 | }), 70 | }), 71 | }); 72 | 73 | const createMutation = gql` 74 | mutation addTodo($text: String!) { 75 | addTodo(text: $text) { 76 | ...TodoFields 77 | } 78 | } 79 | `; 80 | 81 | function updateQueryAfterCreate(prev, { mutationResult }) { 82 | const newTodo = mutationResult.data.addTodo; 83 | return update(prev, { 84 | todos: { 85 | $unshift: [newTodo], 86 | }, 87 | }); 88 | } 89 | 90 | export const withCreateMutation = graphql(createMutation, { 91 | options: () => ({ 92 | fragments: getFragmentDefinitions(TodoFields), 93 | }), 94 | props: ({ mutate }) => ({ 95 | addTodo: ({ text }) => mutate({ 96 | variables: { text }, 97 | optimisticResponse: { 98 | __typename: 'Mutation', 99 | addTodo: { 100 | __typename: 'Todo', 101 | createdAt: new Date().getTime(), 102 | id: Math.floor(Math.random() * 1000).toString(), 103 | text, 104 | isComplete: false, 105 | }, 106 | }, 107 | updateQueries: { 108 | allTodos: updateQueryAfterCreate, 109 | activeTodos: updateQueryAfterCreate, 110 | }, 111 | }), 112 | }), 113 | }); 114 | -------------------------------------------------------------------------------- /app/src/state/todos/queries.js: -------------------------------------------------------------------------------- 1 | import { graphql } from 'react-apollo'; 2 | import { getFragmentDefinitions } from 'apollo-client'; 3 | import gql from 'graphql-tag'; 4 | import { sortTodos } from './helpers'; 5 | import { TodoFields } from './fragments'; 6 | 7 | 8 | const allTodosQuery = gql` 9 | query allTodos { 10 | todos { ...TodoFields } 11 | } 12 | `; 13 | 14 | const completedTodosQuery = gql` 15 | query completedTodos { 16 | todos(isComplete: true) { ...TodoFields } 17 | } 18 | `; 19 | 20 | const activeTodosQuery = gql` 21 | query activeTodos { 22 | todos(isComplete: false) { ...TodoFields } 23 | } 24 | `; 25 | 26 | const basicQueryConfig = { 27 | options: { 28 | fragments: getFragmentDefinitions(TodoFields), 29 | }, 30 | props: ({ data: { loading, todos } }) => ({ 31 | loading, 32 | todos: sortTodos(todos), 33 | }), 34 | }; 35 | 36 | export const withAllTodos = graphql(allTodosQuery, basicQueryConfig); 37 | export const withCompletedTodos = graphql(completedTodosQuery, basicQueryConfig); 38 | export const withActiveTodos = graphql(activeTodosQuery, basicQueryConfig); 39 | -------------------------------------------------------------------------------- /app/src/store.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | // import { fromJS } from 'immutable'; 3 | import createSagaMiddleware from 'redux-saga'; 4 | import createReducer from './state'; 5 | import devTools from 'remote-redux-devtools'; 6 | import apollo from './state/apollo'; 7 | 8 | const sagaMiddleware = createSagaMiddleware(); 9 | 10 | function configureStore(initialState = {}) { 11 | const middlewares = [ 12 | sagaMiddleware, 13 | apollo.middleware(), 14 | ]; 15 | 16 | const enhancers = [ 17 | applyMiddleware(...middlewares), 18 | ]; 19 | 20 | if (__DEV__) { 21 | enhancers.push(devTools()); 22 | } 23 | 24 | const store = createStore( 25 | createReducer(), 26 | initialState, 27 | compose(...enhancers) 28 | ); 29 | 30 | // Extensions 31 | store.runSaga = sagaMiddleware.run; 32 | 33 | return store; 34 | } 35 | 36 | module.exports = configureStore; 37 | -------------------------------------------------------------------------------- /baker/ci/run-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: "off" */ 2 | 3 | // Attempt to run actual npm run setup to make sure all the jazz gets properly bootstrapped 4 | // - get rid of current node_modules dir 5 | // - reinstall deps 6 | // - run app generator 7 | // - bundle app using React Native bundle to check if the build works 8 | 9 | const run = require('child_process').execSync; 10 | const packageJSON = require('../../package'); 11 | const packageName = packageJSON.name; 12 | 13 | 14 | // XX: only try running setup tests if we are testing original 15 | // boilerplate package (baker) and not the app built on top of it 16 | if (packageName === 'baker') { 17 | console.log('removing current node_modules directory...'); 18 | run('rm -rf node_modules'); 19 | 20 | console.log('trying to set up project'); 21 | 22 | console.log('installing deps...'); 23 | run('npm install'); 24 | 25 | console.log('running app generator...'); 26 | // eslint-disable-next-line max-len 27 | const r = run('./node_modules/babel-cli/bin/babel-node.js --presets es2015 ./baker/generate.js app TestApplication --server'); 28 | console.log(r.toString()); 29 | } else { 30 | console.log('skipping setup tests since this is the app based on baker'); 31 | } 32 | 33 | console.log('building for iOS...'); 34 | run('react-native bundle --entry-file index.ios.js --platform ios --bundle-output ./bundle.ios.js'); 35 | 36 | console.log('building for Android...'); 37 | // eslint-disable-next-line max-len 38 | run('react-native bundle --entry-file index.android.js --platform android --bundle-output ./bundle.android.js'); 39 | -------------------------------------------------------------------------------- /baker/generate.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import yeoman from 'yeoman-environment'; 3 | 4 | const argv = require('minimist')(process.argv.slice(2)); 5 | const env = yeoman.createEnv(); 6 | const generators = [ 7 | 'app', 'component', 'container', 'list', 'reducer', 'navigation', 8 | ]; 9 | const supportedCommands = ['app']; 10 | 11 | generators.forEach(g => env.register(`./baker/generators/${g}`, `rn:${g}`)); 12 | 13 | function defaultCommand() { 14 | env.run('rn:list'); 15 | } 16 | 17 | function setupApp() { 18 | const ops = { baker: 'baker' }; 19 | if (argv._.length >= 2) { 20 | ops.name = argv._[1]; 21 | } 22 | 23 | if (argv.server) { 24 | Object.assign(ops, { addServer: true }); 25 | } 26 | 27 | env.run('rn', ops); 28 | } 29 | 30 | function runCommand() { 31 | if (argv._.length !== 0) { 32 | const command = argv._[0]; 33 | 34 | if (supportedCommands.indexOf(command) === -1) { 35 | return; 36 | } 37 | 38 | switch (command) { 39 | case 'app': 40 | setupApp(); 41 | break; 42 | default: 43 | defaultCommand(); 44 | } 45 | } else { 46 | defaultCommand(); 47 | } 48 | } 49 | 50 | runCommand(); 51 | -------------------------------------------------------------------------------- /baker/generators/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-underscore-dangle": "off", 4 | "import/no-extraneous-dependencies": "off", 5 | "import/no-unresolved": "off" 6 | } 7 | } -------------------------------------------------------------------------------- /baker/generators/app/index.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: "off" */ 2 | /* globals which: false */ 3 | 4 | import BaseGenerator from '../base'; 5 | import yosay from 'yosay'; 6 | import 'shelljs/global'; 7 | import fs from 'fs'; 8 | import fsExtra from 'fs-extra'; 9 | 10 | module.exports = BaseGenerator.extend({ 11 | constructor(args, options) { 12 | BaseGenerator.call(this, args, options); 13 | 14 | this.applicationName = this.options.name; 15 | this.addServer = this.options.addServer; 16 | }, 17 | 18 | initializing() { 19 | // XX: check if current directory has smth resembling a bootstrapped RN application 20 | if (this._currentDirectoryHasRNApp()) { 21 | this._abortSetup(); 22 | } 23 | 24 | if (!this._checkIfRNIsInstalled()) { 25 | this.env.error('No React Native found: start by installing it https://facebook.github.io/react-native/docs/getting-started.html#quick-start'); 26 | } 27 | 28 | this.log(yosay( 29 | 'Welcome to the sublime Reactive Native generator!' 30 | )); 31 | 32 | this.option('baker'); 33 | }, 34 | 35 | prompting() { 36 | const prompts = []; 37 | 38 | this.applicationName = this.options.name; 39 | 40 | if (!this.applicationName) { 41 | prompts.push({ 42 | type: 'input', 43 | name: 'name', 44 | message: 'What should your app be called?', 45 | default: 'MyReactApp', 46 | validate: value => (/^[$A-Z_][0-9A-Z_$]*$/i).test(value), 47 | }); 48 | } 49 | 50 | if (typeof this.addServer === 'undefined') { 51 | prompts.push({ 52 | type: 'confirm', 53 | name: 'addServer', 54 | message: 'Do you want a Parse Server setup for this app?', 55 | default: true, 56 | }); 57 | } 58 | 59 | if (prompts.length !== 0) { 60 | const done = this.async(); 61 | this.prompt(prompts, answers => { 62 | if (typeof this.applicationName === 'undefined') { 63 | this.applicationName = answers.name; 64 | } 65 | 66 | if (typeof this.addServer === 'undefined') { 67 | this.addServer = answers.addServer; 68 | } 69 | 70 | done(); 71 | }); 72 | } 73 | }, 74 | 75 | makeSureDestinationDirectoryIsNotOccupiedAlready() { 76 | // check to see if destination directory is empty or not 77 | // if it's empty -> go ahead and do the thing 78 | // if not empty -> create a folder and use it as cwd 79 | // except if this.options.baker is on 80 | 81 | const filesInDestinationDirectory = fs.readdirSync(this.destinationPath('.')); 82 | if (filesInDestinationDirectory.length !== 0 && !this.options.baker) { 83 | fs.mkdirSync(this.destinationPath(this.applicationName)); 84 | this.destinationRoot(this.destinationPath(this.applicationName)); 85 | } 86 | }, 87 | 88 | writing: { 89 | packageJSON() { 90 | const packageJSONPath = this.destinationPath('package.json'); 91 | const packageJSON = { 92 | name: this.applicationName, 93 | engines: { 94 | node: '>=4.3', 95 | }, 96 | scripts: { 97 | 'build-ios': 'node node_modules/react-native/local-cli/cli.js bundle --entry-file index.ios.js --bundle-output iOS/main.jsbundle --platform "ios" --assets-dest ./ --dev false --reset-cache', 98 | 'build-android': 'node node_modules/react-native/local-cli/cli.js bundle --entry-file index.android.js --bundle-output iOS/main.jsbundle --platform "android" --assets-dest ./ --dev false --reset-cache', 99 | ios: 'node node_modules/react-native/local-cli/cli.js run-ios', 100 | android: 'node node_modules/react-native/local-cli/cli.js run-android', 101 | 'test:app': `./node_modules/mocha/bin/mocha ${this.appDirectory}/tests.js ./app/**/*.test.js`, 102 | }, 103 | dependencies: { 104 | react: '~15.2.0', 105 | 'react-native': '^0.29.0', 106 | 'react-redux': '^4.4.5', 107 | redux: '^3.5.2', 108 | immutable: '^3.8.1', 109 | 'redux-immutable': '^3.0.6', 110 | reselect: '^2.5.1', 111 | 'react-native-navigation-redux-helpers': '^0.3.0', 112 | 'redux-saga': '^0.11.0', 113 | }, 114 | devDependencies: { 115 | }, 116 | }; 117 | 118 | if (this.addServer) { 119 | Object.assign(packageJSON.dependencies, { 120 | express: '^4.13.4', 121 | graphql: '^0.6.0', 122 | parse: '1.8.5', 123 | 'parse-dashboard': '^1.0.13', 124 | 'parse-graphql-client': '^0.2.0', 125 | 'parse-graphql-server': '^0.3.0', 126 | 'parse-server': '^2.2.11', 127 | }); 128 | 129 | Object.assign(packageJSON.devDependencies, { 130 | 'babel-watch': '^2.0.2', 131 | 'babel-preset-react-native': '^1.9.0', 132 | 'mongodb-runner': '^3.3.2', 133 | 'react-addons-test-utils': '^15.2.1', 134 | 'react-dom': '^15.2.1', 135 | 'react-native-mock': '^0.2.4', 136 | enzyme: '^2.4.1', 137 | }); 138 | 139 | Object.assign(packageJSON.scripts, { 140 | mongo: 'node ./node_modules/mongodb-runner/bin/mongodb-runner start --name=dev --purge false', 141 | server: 'npm run mongo && NODE_ENV=development ./baker/scripts/server.js', 142 | 'server-debug': 'npm run mongo && NODE_ENV=development ./baker/scripts/server.js --debug', 143 | 'server-watch': 'npm run mongo && NODE_ENV=development ./baker/scripts/server.js --watch', 144 | }); 145 | } 146 | 147 | try { 148 | fs.statSync(packageJSONPath); 149 | 150 | // merge current package.json in the dest directory with packageJSON 151 | const originalPackageJSON = fsExtra.readJsonSync(packageJSONPath); 152 | 153 | const json = Object.assign( 154 | packageJSON, { 155 | scripts: Object.assign({}, originalPackageJSON.scripts, packageJSON.scripts), 156 | dependencies: Object.assign({}, originalPackageJSON.dependencies, packageJSON.dependencies), 157 | devDependencies: Object.assign({}, originalPackageJSON.devDependencies, packageJSON.devDependencies), 158 | } 159 | ); 160 | 161 | this.conflicter.force = true; 162 | this.fs.writeJSON(json); 163 | } catch (e) { 164 | // no package.json in the target directory 165 | this.fs.writeJSON(packageJSONPath, packageJSON); 166 | } 167 | }, 168 | 169 | serverFiles() { 170 | if (!this.addServer) { 171 | return; 172 | } 173 | 174 | this.bulkDirectory('server', this.serverDirectory); 175 | this.bulkDirectory('settings', this.settingsDirectory); 176 | this.copy('settings.js', `${this.appDirectory}/settings.js`); 177 | 178 | this.composeWith('model', { 179 | options: { 180 | modelName: 'Example', 181 | }, 182 | }, { 183 | local: require.resolve('../model'), 184 | }); 185 | }, 186 | }, 187 | 188 | install: { 189 | setupRN() { 190 | this.installDependencies({ 191 | bower: false, 192 | callback: () => { 193 | this._initRN(); 194 | }, 195 | }); 196 | }, 197 | }, 198 | 199 | end() { 200 | this.conflicter.force = true; 201 | 202 | ['ios', 'android'].forEach(platform => { 203 | this.template('index.js.hbs', `index.${platform}.js`, 204 | { 205 | applicationName: this.applicationName, 206 | } 207 | ); 208 | }); 209 | 210 | this.bulkDirectory('app', this.appDirectory); 211 | this.composeWith('component', { 212 | options: { 213 | componentName: 'App', 214 | destinationRoot: this.destinationPath('.'), 215 | boilerplateName: 'Vanila', 216 | platformSpecific: false, 217 | }, 218 | }, { 219 | local: require.resolve('../component'), 220 | }); 221 | }, 222 | 223 | _checkIfRNIsInstalled() { 224 | return which('react-native'); 225 | }, 226 | 227 | _initRN() { 228 | this.spawnCommandSync('node', [ 229 | this.templatePath('setup-rn.js'), 230 | this.destinationRoot(), 231 | this.applicationName, 232 | ]); 233 | }, 234 | 235 | _currentDirectoryHasRNApp() { 236 | try { 237 | ['android', 'ios', 'index.ios.js', 'index.android.js'].forEach(f => fs.statSync(this.destinationPath(f))); 238 | return true; 239 | } catch (e) { 240 | return false; 241 | } 242 | }, 243 | 244 | _abortSetup() { 245 | this.env.error('Yo! Looks like you are trying to run the app generator in a directory that already has a RN app.'); 246 | }, 247 | }); 248 | -------------------------------------------------------------------------------- /baker/generators/app/templates/app/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | 3 | // XX: Do not rename this variable if you want reducer generator 4 | // to keep working properly (and you do want that, right?) 5 | const applicationReducers = { 6 | removeThisReducerOnceYouAddALegitOne: () => ({}), 7 | }; 8 | 9 | export default function createReducer() { 10 | return combineReducers(applicationReducers); 11 | } 12 | -------------------------------------------------------------------------------- /baker/generators/app/templates/app/sagas/index.js: -------------------------------------------------------------------------------- 1 | /* eslint eol-last:"off", comma-dangle:"off" */ 2 | 3 | // XX: this is used by the code generator 4 | // please do not rename this 5 | const sagas = [ 6 | ]; 7 | 8 | module.exports = sagas; 9 | -------------------------------------------------------------------------------- /baker/generators/app/templates/app/setup.js: -------------------------------------------------------------------------------- 1 | import App from './components/App'; 2 | import React, { Component } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import configureStore from './store'; 5 | 6 | const store = configureStore(); 7 | 8 | function setup() { 9 | class Root extends Component { 10 | render() { 11 | return ( 12 | 13 | 14 | 15 | ); 16 | } 17 | } 18 | 19 | return Root; 20 | } 21 | 22 | module.exports = setup; 23 | -------------------------------------------------------------------------------- /baker/generators/app/templates/app/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, compose, applyMiddleware } from 'redux'; 2 | import { fromJS } from 'immutable'; 3 | import createReducer from './reducers'; 4 | import sagas from './sagas'; 5 | import createSagaMiddleware from 'redux-saga'; 6 | import devTools from 'remote-redux-devtools'; 7 | 8 | const sagaMiddleware = createSagaMiddleware(); 9 | 10 | function configureStore(initialState = fromJS({})) { 11 | const enhancers = [ 12 | applyMiddleware(sagaMiddleware), 13 | ]; 14 | 15 | if (__DEV__) { 16 | enhancers.push(devTools()); 17 | } 18 | 19 | // const createStoreWithMiddleware = compose(...middleware)(createStore); 20 | // return createStoreWithMiddleware(createReducer(), initialState); 21 | const store = createStore(createReducer(), initialState, compose(...enhancers)); 22 | 23 | sagas.forEach(saga => sagaMiddleware.run(saga)); 24 | 25 | return store; 26 | } 27 | 28 | module.exports = configureStore; 29 | -------------------------------------------------------------------------------- /baker/generators/app/templates/app/tests.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: "off", prefer-arrow-callback: "off" */ 2 | 3 | require('babel-register')({ 4 | presets: ['es2015', 'react-native'], 5 | }); 6 | require('react-native-mock/mock'); 7 | 8 | global.chai = require('chai'); 9 | global.sinon = require('sinon'); 10 | global.chai.use(require('sinon-chai')); 11 | 12 | function testSetup(root) { 13 | root.expect = root.chai.expect; 14 | 15 | beforeEach(function be() { 16 | // Using these globally-available Sinon features is preferrable, as they're 17 | // automatically restored for you in the subsequent `afterEach` 18 | root.sandbox = root.sinon.sandbox.create(); 19 | root.sinon = root.sinon; 20 | root.stub = root.sandbox.stub.bind(root.sandbox); 21 | root.spy = root.sandbox.spy.bind(root.sandbox); 22 | root.mock = root.sandbox.mock.bind(root.sandbox); 23 | root.useFakeTimers = root.sandbox.useFakeTimers.bind(root.sandbox); 24 | root.useFakeXMLHttpRequest = root.sandbox.useFakeXMLHttpRequest.bind(root.sandbox); 25 | root.useFakeServer = root.sandbox.useFakeServer.bind(root.sandbox); 26 | }); 27 | 28 | afterEach(function ae() { 29 | delete root.stub; 30 | delete root.spy; 31 | root.sandbox.restore(); 32 | }); 33 | } 34 | 35 | testSetup(global); 36 | -------------------------------------------------------------------------------- /baker/generators/app/templates/index.js.hbs: -------------------------------------------------------------------------------- 1 | const { AppRegistry } = require('react-native'); 2 | const setup = require('./{{appDirectory}}/setup'); 3 | 4 | AppRegistry.registerComponent('{{applicationName}}', setup); 5 | -------------------------------------------------------------------------------- /baker/generators/app/templates/server/graphql/index.js: -------------------------------------------------------------------------------- 1 | import schema from './schema'; 2 | import parseGraphQLHTTP from 'parse-graphql-server'; 3 | 4 | export default { 5 | setup(app, graphiql = false) { 6 | app.use('/graphql', parseGraphQLHTTP({ schema, graphiql })); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /baker/generators/app/templates/server/graphql/schema.js: -------------------------------------------------------------------------------- 1 | /* eslint comma-dangle: "off", prefer-template: "off", eol-last: "off" */ 2 | import { 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | } from 'graphql'; 6 | import assert from 'assert'; 7 | 8 | const queries = {}; 9 | const mutations = []; 10 | 11 | // XX: check for duplicate mutation declarations 12 | // accross different models 13 | function checkForDuplicates(listOfMutations) { 14 | const existingMutations = []; 15 | listOfMutations.forEach(ms => Object.keys(ms).forEach(m => { 16 | assert(existingMutations.indexOf(m) === -1, 'Duplicate mutation declaration:' + m); 17 | existingMutations.push(m); 18 | })); 19 | } 20 | checkForDuplicates(mutations); 21 | 22 | export default new GraphQLSchema({ 23 | query: new GraphQLObjectType({ 24 | name: 'Query', 25 | fields: queries 26 | }), 27 | mutation: new GraphQLObjectType({ 28 | name: 'Mutation', 29 | fields: Object.assign.apply(this, [ 30 | {}, 31 | ...mutations 32 | ]) 33 | }) 34 | }); 35 | -------------------------------------------------------------------------------- /baker/generators/app/templates/server/index.js: -------------------------------------------------------------------------------- 1 | import settings from '../settings/development'; 2 | import packageJSON from '../package'; 3 | import express from 'express'; 4 | import graphql from './graphql'; 5 | import parseServer from './parse-server'; 6 | 7 | const app = express(); 8 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 9 | 10 | parseServer.setup(app, packageJSON.name, settings, IS_DEVELOPMENT); 11 | graphql.setup(app, IS_DEVELOPMENT); 12 | 13 | app.listen(settings.serverPort, () => { 14 | console.log(`server running on port ${settings.serverPort}`); 15 | }); 16 | -------------------------------------------------------------------------------- /baker/generators/app/templates/server/parse-server/index.js: -------------------------------------------------------------------------------- 1 | import { ParseServer } from 'parse-server'; 2 | import Parse from 'parse/node'; 3 | import ParseDashboard from 'parse-dashboard'; 4 | 5 | export default { 6 | setup(app, appName, settings, allowInsecureHTTPInParseDashboard = false) { 7 | Parse.initialize(settings.parseServerApplicationId, 'js-key', settings.parseServerMasterKey); 8 | Parse.serverURL = settings.parseServerURL; 9 | 10 | const api = new ParseServer({ 11 | appId: settings.parseServerApplicationId, 12 | masterKey: settings.parseServerMasterKey, 13 | serverURL: settings.parseServerURL, 14 | databaseURI: settings.parseServerDatabaseURI, 15 | }); 16 | 17 | app.use('/parse', api); 18 | 19 | app.use( 20 | '/dashboard', 21 | // eslint-disable-next-line new-cap 22 | ParseDashboard({ 23 | apps: [{ 24 | serverURL: settings.parseServerURL, 25 | appId: settings.parseServerApplicationId, 26 | masterKey: settings.parseServerMasterKey, 27 | appName, 28 | iconName: 'logo.png', 29 | }], 30 | iconsFolder: 'server/public/images', 31 | users: settings.parseServerDashboardUsers, 32 | }, allowInsecureHTTPInParseDashboard) 33 | ); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /baker/generators/app/templates/server/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/baker/generators/app/templates/server/public/images/logo.png -------------------------------------------------------------------------------- /baker/generators/app/templates/settings.js: -------------------------------------------------------------------------------- 1 | import base from '../settings/development'; 2 | import iosOverrides from '../settings/development.ios'; 3 | import androidOverrides from '../settings/development.android'; 4 | import { Platform } from 'react-native'; 5 | 6 | export default { 7 | load() { 8 | return Object.assign({}, base, Platform.select({ 9 | ios: iosOverrides, 10 | android: androidOverrides, 11 | })); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /baker/generators/app/templates/settings/development.android.json: -------------------------------------------------------------------------------- 1 | { 2 | "parseServerURL": "http://10.0.2.2:8000/parse", 3 | "graphqlURL": "http://10.0.2.2:8000/graphql" 4 | } -------------------------------------------------------------------------------- /baker/generators/app/templates/settings/development.ios.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /baker/generators/app/templates/settings/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverPort": 8000, 3 | "parseServerApplicationId": "parse-app-id", 4 | "parseServerMasterKey": "70c6093dba5a7e55968a1c7ad3dd3e5a74ef5cac", 5 | "parseServerDatabaseURI": "mongodb://localhost:27017/dev", 6 | "parseServerURL": "http://localhost:8000/parse", 7 | "parseServerDashboardUsers": [ 8 | { 9 | "user": "admin", 10 | "pass": "admin" 11 | } 12 | ], 13 | "graphqlURL": "http://localhost:8000/graphql" 14 | } -------------------------------------------------------------------------------- /baker/generators/app/templates/setup-rn.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint no-var: "off" */ 4 | 5 | // XX: Not really a template 6 | // usage: node setup-rn.js /path/to/project/root projectName 7 | 8 | var path = require('path'); 9 | var clipath = path.resolve(process.argv[2], 10 | 'node_modules', 11 | 'react-native', 12 | 'cli.js' 13 | ); 14 | var cli = require(clipath); 15 | cli.init(process.argv[2], process.argv[3]); 16 | -------------------------------------------------------------------------------- /baker/generators/base.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import yeoman from 'yeoman-generator'; 3 | import _ from 'lodash'; 4 | import changeCase from 'change-case'; 5 | import fs from 'fs'; 6 | import Handlebars from 'handlebars'; 7 | import esprima from 'esprima'; 8 | import escodegen from 'escodegen'; 9 | import escodegenOptions from './escodegen'; 10 | import esprimaOptions from './esprima'; 11 | import namingConventions from './naming'; 12 | import shell from 'shelljs'; 13 | 14 | module.exports = yeoman.Base.extend({ 15 | constructor(...args) { 16 | yeoman.Base.apply(this, args); 17 | 18 | // eslint-disable-next-line global-require 19 | const boilerplates = require('./boilerplates'); 20 | this.runBoilerplateHook = boilerplates.runBoilerplateHook; 21 | this.runBoilerplateBeforeHook = boilerplates.runBoilerplateBeforeHook; 22 | this.runBoilerplateAfterHook = boilerplates.runBoilerplateAfterHook; 23 | 24 | this.appDirectory = 'app'; 25 | this.serverDirectory = 'server'; 26 | this.settingsDirectory = 'settings'; 27 | this.platforms = ['ios', 'android']; 28 | this.namingConventions = namingConventions; 29 | this.Handlebars = Handlebars; 30 | // XX: navigation generator needs these to 31 | // list available navigation boilerplates 32 | // since they are explicitly excluded from component generator 33 | this.navigationBoilerplates = [ 34 | 'navigation/Cards', 35 | 'navigation/Tabs', 36 | ]; 37 | 38 | this.Handlebars.registerHelper('uppercaseFirst', text => changeCase.upperCaseFirst(text)); 39 | this.Handlebars.registerHelper('pascalCase', text => changeCase.pascalCase(text)); 40 | this.Handlebars.registerHelper('camelCase', text => changeCase.camelCase(text)); 41 | this.Handlebars.registerHelper('constantCase', text => changeCase.constantCase(text)); 42 | 43 | this.template = (source, destination, data) => { 44 | // XX: override Yo's standard template method to use Handlebars templates 45 | const template = Handlebars.compile(this.read(source)); 46 | const content = template(_.extend({}, this, data || {})); 47 | this.write(destination, content); 48 | }; 49 | 50 | this.parseJSSource = content => { 51 | let tree = esprima.parse(content, esprimaOptions); 52 | tree = escodegen.attachComments(tree, tree.comments, tree.tokens); 53 | return tree; 54 | }; 55 | 56 | this.generateJSFile = (ast, path) => { 57 | const content = escodegen.generate(ast, escodegenOptions); 58 | this.write(path, content); 59 | }; 60 | }, 61 | 62 | _fileExists(fullFilePath) { 63 | try { 64 | fs.statSync(fullFilePath); 65 | return true; 66 | } catch (e) { 67 | return false; 68 | } 69 | }, 70 | 71 | _readFile(fullFilePath) { 72 | return fs.readFileSync(fullFilePath).toString(); 73 | }, 74 | 75 | _dropHBSExtension(fileName) { 76 | const parts = fileName.split('.hbs'); 77 | return parts.length === 2 ? parts[0] : fileName; 78 | }, 79 | 80 | _listAvailableBoilerPlates() { 81 | const excludeBoilerplates = [...this.navigationBoilerplates]; 82 | const boilerplatesPath = this.templatePath('./boilerplates'); 83 | 84 | const boilerplates = _.uniq( 85 | shell.find(boilerplatesPath).filter(file => file.match(/\.js.hbs$/i)) 86 | .map(file => (/\/([a-zA-Z0-9\/]+)(\.ios|\.android)?\.js\.hbs$/ig).exec( 87 | file.split(boilerplatesPath)[1])[1] 88 | ) 89 | ); 90 | 91 | return boilerplates.filter(b => excludeBoilerplates.indexOf(b) === -1); 92 | }, 93 | 94 | _renderBoilerplate(boilerplate, platform) { 95 | let template; 96 | try { 97 | // see if there's a boiler plate for this specific platorm 98 | template = this.read(`./boilerplates/${boilerplate}.${platform}.js.hbs`); 99 | } catch (e) { 100 | template = this.read(`./boilerplates/${boilerplate}.js.hbs`); 101 | } 102 | 103 | return Handlebars.compile(template)(this); 104 | }, 105 | 106 | _isBoilerplatePlatformSpecific(boilerplate) { 107 | try { 108 | this.platforms.forEach(platform => { 109 | fs.statSync(this.templatePath(`./boilerplates/${boilerplate}.${platform}.js.hbs`)); 110 | }); 111 | return true; 112 | } catch (e) { 113 | return false; 114 | } 115 | }, 116 | 117 | dummyMethod() { 118 | // XX: keep this here so tests can run against base generator 119 | }, 120 | }); 121 | -------------------------------------------------------------------------------- /baker/generators/boilerplates.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | runBoilerplateHook(boilerplate, hookType) { 3 | try { 4 | const hookModuleLocation = this.templatePath(`./boilerplates/${boilerplate}__hook.js`); 5 | // eslint-disable-next-line global-require 6 | const moduleHook = require(hookModuleLocation); 7 | 8 | switch (hookType) { 9 | case 'before': 10 | moduleHook.before(this); 11 | break; 12 | case 'after': 13 | moduleHook.after(this); 14 | break; 15 | default: 16 | throw new Error('Invalid hook type', hookType); 17 | } 18 | } catch (e) { 19 | // no hooks 20 | } 21 | }, 22 | 23 | runBoilerplateBeforeHook(boilerplate) { 24 | this.runBoilerplateHook(boilerplate, 'before'); 25 | }, 26 | 27 | runBoilerplateAfterHook(boilerplate) { 28 | this.runBoilerplateHook(boilerplate, 'after'); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /baker/generators/component/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | 3 | module.exports = BaseGenerator.extend({ 4 | constructor(args, options) { 5 | BaseGenerator.call(this, args, options); 6 | 7 | this.isContainer = options.isContainer; 8 | this.componentName = options.componentName; 9 | this.boilerplateName = options.boilerplateName; 10 | this.addReducer = options.addReducer; 11 | this.platformSpecific = options.platformSpecific; 12 | this.doNotGenerateTests = options.doNotGenerateTests; 13 | 14 | if (options.destinationRoot) { 15 | this.destinationRoot(options.destinationRoot); 16 | } 17 | 18 | this.Handlebars.registerPartial('mapDispatchAndPropsAndConnect', 19 | this.read(this.templatePath('partials/mapDispatchPropsAndConnect.js.hbs')) 20 | ); 21 | }, 22 | 23 | prompting() { 24 | const done = this.async(); 25 | const prompts = []; 26 | 27 | if (!this.componentName) { 28 | prompts.push({ 29 | type: 'input', 30 | name: 'componentName', 31 | message: 'What should your component be called?', 32 | default: 'MyNewComponent', 33 | validate: value => this.namingConventions.componentName.regEx.test(value), 34 | }); 35 | } 36 | 37 | if (!this.boilerplateName) { 38 | prompts.push({ 39 | type: 'list', 40 | name: 'boilerplateName', 41 | message: 'Which boilerplate do you want to use?', 42 | default: 'Vanila', 43 | choices: () => this._listAvailableBoilerPlates(), 44 | }); 45 | } 46 | 47 | if (typeof this.platformSpecific === 'undefined') { 48 | prompts.push({ 49 | type: 'confirm', 50 | name: 'platformSpecific', 51 | message: 'Do you separate versions of this component for iOS and Android?', 52 | when: answers => { 53 | const boilerplateName = this.boilerplateName || answers.boilerplateName; 54 | return !this._isBoilerplatePlatformSpecific(boilerplateName); 55 | }, 56 | default: false, 57 | }); 58 | } 59 | 60 | if (prompts.length === 0) { 61 | done(); 62 | return; 63 | } 64 | 65 | this.prompt(prompts, answers => { 66 | if (answers.componentName) { 67 | this.componentName = answers.componentName; 68 | } 69 | 70 | if (typeof answers.platformSpecific !== 'undefined') { 71 | this.platformSpecific = answers.platformSpecific; 72 | } 73 | 74 | if (answers.boilerplateName) { 75 | this.boilerplateName = answers.boilerplateName; 76 | } 77 | 78 | done(); 79 | }); 80 | }, 81 | 82 | configuring: { 83 | platforms() { 84 | if (this._isBoilerplatePlatformSpecific(this.boilerplateName)) { 85 | this.platformSpecific = true; 86 | } 87 | }, 88 | 89 | files() { 90 | this.componentName = this.namingConventions.componentName.clean( 91 | this.componentName 92 | ); 93 | 94 | this.files = [ 95 | 'styles.js.hbs', 96 | ]; 97 | 98 | if (!this.platformSpecific && !this.doNotGenerateTests) { 99 | this.files.push('index.test.js.hbs'); 100 | } 101 | }, 102 | 103 | reducer() { 104 | this.reducerName = this.namingConventions.reducerName.clean( 105 | this.componentName 106 | ); 107 | }, 108 | 109 | selector() { 110 | if (this.addReducer) { 111 | this.selectorName = this.namingConventions.selectorName.clean( 112 | this.componentName 113 | ); 114 | } 115 | }, 116 | }, 117 | 118 | writing: { 119 | reducer() { 120 | if (this.addReducer) { 121 | this.composeWith('rn:reducer', { 122 | options: { 123 | container: this.componentName, 124 | boilerplateName: this.boilerplateName, 125 | doNotGenerateTests: this.doNotGenerateTests, 126 | }, 127 | }, { 128 | local: require.resolve('../reducer'), 129 | }); 130 | } 131 | }, 132 | 133 | everything() { 134 | this.files.forEach(f => { 135 | this.template(f, 136 | `${this.appDirectory}/components/${this.componentName}/${this._dropHBSExtension(f)}`); 137 | }); 138 | 139 | this.runBoilerplateBeforeHook(this.boilerplateName); 140 | 141 | if (this.platformSpecific) { 142 | this.platforms.forEach(platform => { 143 | const path = `${this.appDirectory}/components/${this.componentName}`; 144 | 145 | this.template('index.js.hbs', `${path}/index.${platform}.js`, 146 | Object.assign({}, this, { 147 | boilerplate: this._renderBoilerplate(this.boilerplateName, platform), 148 | }) 149 | ); 150 | 151 | if (!this.doNotGenerateTests) { 152 | this.template('index.test.js.hbs', `${path}/index.${platform}.test.js`, 153 | Object.assign({}, this, { 154 | platform, 155 | }) 156 | ); 157 | } 158 | }); 159 | } else { 160 | const path = `${this.appDirectory}/components/${this.componentName}/index.js`; 161 | this.template('index.js.hbs', path, 162 | Object.assign({}, this, { 163 | boilerplate: this._renderBoilerplate(this.boilerplateName), 164 | }) 165 | ); 166 | } 167 | 168 | this.runBoilerplateAfterHook(this.boilerplateName); 169 | }, 170 | }, 171 | }); 172 | -------------------------------------------------------------------------------- /baker/generators/component/templates/boilerplates/Vanila.js.hbs: -------------------------------------------------------------------------------- 1 | const { View, Text } = ReactNative; 2 | 3 | {{#if isContainer}}export {{/if}}class {{componentName}} extends Component { 4 | render() { 5 | return ( 6 | 7 | {{componentName}} 8 | 9 | ); 10 | } 11 | } 12 | 13 | {{> mapDispatchAndPropsAndConnect }} -------------------------------------------------------------------------------- /baker/generators/component/templates/boilerplates/navigation/Cards.js.hbs: -------------------------------------------------------------------------------- 1 | 2 | const { View, Text, NavigationExperimental } = ReactNative; 3 | const { CardStack: NavigationCardStack } = NavigationExperimental; 4 | 5 | export class {{componentName}} extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.renderScene = this.renderScene.bind(this); 10 | this.renderOverlay = this.renderOverlay.bind(this); 11 | } 12 | 13 | // XX: get rid of this eslint-disable thingy once you set up 14 | // proper rendering for your scene 15 | // eslint-disable-next-line no-unused-vars 16 | renderScene(props) { 17 | // check *props.scene.route.key* to figure out 18 | // which scene shoud be rendered, for example 19 | // 20 | // if (props.scene.route.key === 'applicationTabs') { 21 | // return ( 22 | // 23 | // 24 | // 25 | // ); 26 | // } 27 | // 28 | // To navigate around use pushRoute and popRoute actions: 29 | // import { actions as navigationActions } from 'react-native-navigation-redux-helpers'; 30 | // const { 31 | // popRoute, 32 | // pushRoute 33 | // } = navigationActions; 34 | // 35 | // pushRoute({ key: 'route smth'}, this.props.{{reducerName}}.key); 36 | // popRoute(this.props.{{reducerName}}.key); 37 | 38 | 39 | return ( 40 | 41 | Generic scene in {{componentName}} 42 | 43 | ); 44 | } 45 | 46 | renderOverlay() { 47 | return null; 48 | } 49 | 50 | render() { 51 | const navigationState = this.props.{{reducerName}}; 52 | return ( 53 | {}} 60 | /> 61 | ); 62 | } 63 | } 64 | 65 | 66 | {{> mapDispatchAndPropsAndConnect }} -------------------------------------------------------------------------------- /baker/generators/component/templates/boilerplates/navigation/Tabs.android.js.hbs: -------------------------------------------------------------------------------- 1 | import { actions as navigationActions } from 'react-native-navigation-redux-helpers'; 2 | 3 | const { View, Text, TouchableHighlight, DrawerLayoutAndroid, ToolbarAndroid } = ReactNative; 4 | const { jumpTo } = navigationActions; 5 | 6 | const androidToolbarStyle = { 7 | backgroundColor: '#E9EAED', 8 | height: 56, 9 | }; 10 | 11 | export class {{componentName}} extends Component { 12 | // XX: get rid of this eslint-disable thingy once you set up 13 | // proper rendering for your tabs 14 | // eslint-disable-next-line no-unused-vars 15 | renderTabContent(tab) { 16 | // XX: replace this with code to render specific components/containers 17 | // corresponding to tabs in your app, e.g. 18 | // if (tab.key === 'maps') { 19 | // return ; 20 | // } 21 | 22 | return ( 23 | 24 | Generic Tab 25 | 26 | ); 27 | } 28 | 29 | renderContent() { 30 | const selectedTab = this.props.{{reducerName}}.routes[this.props.{{reducerName}}.index]; 31 | const navigationIcon = { uri: 'http://placehold.it/56x56' }; 32 | return ( 33 | 34 | this.drawer.openDrawer()} 38 | title={selectedTab.title} 39 | /> 40 | {this.renderTabContent(selectedTab)} 41 | 42 | ); 43 | } 44 | 45 | render() { 46 | const { {{reducerName}}, dispatch } = this.props; 47 | const onNavigate = (action) => { 48 | this.drawer.closeDrawer(); 49 | dispatch(action); 50 | }; 51 | const routes = {{reducerName}}.routes; 52 | 53 | const navigationView = ( 54 | 55 | {routes.map((t, i) => 56 | ( 57 | onNavigate(jumpTo(i, {{reducerName}}.key))} 59 | key={t.key} 60 | > 61 | {t.title} 62 | 63 | ) 64 | )} 65 | 66 | ); 67 | 68 | return ( 69 | { this.drawer = drawer; }} 71 | drawerWidth={300} 72 | drawerPosition={DrawerLayoutAndroid.positions.Left} 73 | renderNavigationView={() => navigationView} 74 | > 75 | {this.renderContent()} 76 | 77 | ); 78 | } 79 | } 80 | 81 | {{> mapDispatchAndPropsAndConnect }} -------------------------------------------------------------------------------- /baker/generators/component/templates/boilerplates/navigation/Tabs.ios.js.hbs: -------------------------------------------------------------------------------- 1 | import { actions as navigationActions } from 'react-native-navigation-redux-helpers'; 2 | 3 | const { View, Text, TabBarIOS } = ReactNative; 4 | const { jumpTo } = navigationActions; 5 | 6 | export class {{componentName}} extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.renderTabContent = this.renderTabContent.bind(this); 11 | } 12 | 13 | // XX: get rid of this eslint-disable thingy once you set up 14 | // proper rendering for your tabs 15 | // eslint-disable-next-line no-unused-vars 16 | renderTabContent(tab) { 17 | // XX: replace this with code to render specific components/containers 18 | // corresponding to tabs in your app, e.g. 19 | // if (tab.key === 'maps') { 20 | // return ; 21 | // } 22 | 23 | return ( 24 | 25 | Generic Tab 26 | 27 | ); 28 | } 29 | 30 | render() { 31 | const { dispatch, {{reducerName}} } = this.props; 32 | const children = {{reducerName}}.routes.map((tab, i) => 33 | ( 34 | dispatch(jumpTo(i, {{reducerName}}.key)) 41 | } 42 | selected={this.props.{{reducerName}}.index === i} 43 | > 44 | {this.renderTabContent(tab)} 45 | 46 | ) 47 | ); 48 | return ( 49 | 50 | {children} 51 | 52 | ); 53 | } 54 | } 55 | 56 | {{> mapDispatchAndPropsAndConnect }} -------------------------------------------------------------------------------- /baker/generators/component/templates/index.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{componentName}} 4 | * 5 | */ 6 | 7 | import ReactNative from 'react-native'; 8 | import React, { Component } from 'react'; 9 | import styles from './styles'; 10 | 11 | {{#if isContainer}} 12 | import { connect } from 'react-redux'; 13 | {{#if selectorName}} 14 | import { createSelector } from 'reselect'; 15 | import { {{selectorName}} } from './reducer'; 16 | {{/if}} 17 | {{/if}} 18 | {{{boilerplate}}} -------------------------------------------------------------------------------- /baker/generators/component/templates/index.test.js.hbs: -------------------------------------------------------------------------------- 1 | import React, { Text } from 'react-native'; 2 | import { shallow } from 'enzyme'; 3 | import {{#if isContainer}}{ {{componentName}} }{{else}}{{componentName}}{{/if}} from './index{{#if platform}}.{{platform}}{{/if}}'; 4 | 5 | describe('{{componentName}} component{{#if platform}} - {{platform}} version{{/if}}', () => { 6 | it('has 1 Text element', () => { 7 | {{#if isContainer}} 8 | const dispatchSpy = spy(); 9 | const containerData = {}; 10 | const wrapper = shallow( 11 | <{{componentName}} dispatch={dispatchSpy} {{reducerName}}={containerData} /> 12 | ); 13 | expect(wrapper.find(Text)).to.have.lengthOf(1); 14 | {{else}} 15 | const wrapper = shallow( 16 | <{{componentName}} /> 17 | ); 18 | expect(wrapper.find(Text)).to.have.lengthOf(1); 19 | {{/if}} 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /baker/generators/component/templates/partials/mapDispatchPropsAndConnect.js.hbs: -------------------------------------------------------------------------------- 1 | {{#if isContainer}} 2 | {{componentName}}.propTypes = { 3 | {{reducerName}}: React.PropTypes.object.isRequired, 4 | dispatch: React.PropTypes.func.isRequired, 5 | }; 6 | {{/if}} 7 | 8 | {{#if isContainer}} 9 | function mapDispatchToProps(dispatch) { 10 | return { 11 | dispatch, 12 | }; 13 | } 14 | {{/if}} 15 | 16 | {{#if selectorName}} 17 | export default connect( 18 | createSelector({{selectorName}}, ({{reducerName}}) => ({ {{reducerName}} })), 19 | mapDispatchToProps{{#if mergeProps}}, mergeProps{{/if}} 20 | )({{componentName}}); 21 | {{else}} 22 | {{#if isContainer}} 23 | function mapStateToProps(state) { 24 | return {}; 25 | } 26 | export default connect(mapStateToProps, mapDispatchToProps{{#if mergeProps}}, mergeProps{{/if}})({{componentName}}); 27 | {{else}} 28 | export default {{componentName}}; 29 | {{/if}} 30 | {{/if}} -------------------------------------------------------------------------------- /baker/generators/component/templates/styles.js.hbs: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native'; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /baker/generators/container/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | 3 | module.exports = BaseGenerator.extend({ 4 | constructor(args, options) { 5 | BaseGenerator.call(this, args, options); 6 | 7 | this.containerName = options.name; 8 | this.selectorName = null; 9 | this.boilerplateName = options.boilerplateName; 10 | this.addReducer = options.addReducer; 11 | this.doNotGenerateTests = options.doNotGenerateTests; 12 | }, 13 | 14 | prompting() { 15 | const done = this.async(); 16 | const prompts = []; 17 | 18 | if (!this.containerName) { 19 | prompts.push({ 20 | type: 'input', 21 | name: 'containerName', 22 | message: 'What should your container be called?', 23 | default: 'MyNewContainer', 24 | validate: value => this.namingConventions.componentName.regEx.test(value), 25 | }); 26 | } 27 | 28 | if (typeof this.addReducer === 'undefined') { 29 | prompts.push({ 30 | type: 'confirm', 31 | name: 'addReducer', 32 | message: 'Do you want a reducer + actions + constants generated?', 33 | default: true, 34 | }); 35 | } 36 | 37 | prompts.push({ 38 | type: 'input', 39 | name: 'selectorName', 40 | message: 'What is the name for the new selector?', 41 | default: answers => this.namingConventions.selectorName.clean(answers.containerName), 42 | validate: value => (/^[$A-Z_][0-9A-Z_$]*$/i).test(value), 43 | when: answers => answers.containerSelectorName === 'New Selector', 44 | }); 45 | 46 | if (prompts.length === 0) { 47 | done(); 48 | return; 49 | } 50 | 51 | this.prompt(prompts, answers => { 52 | if (answers.containerName) { 53 | this.containerName = answers.containerName; 54 | } 55 | 56 | if (typeof this.addReducer === 'undefined') { 57 | this.addReducer = answers.addReducer; 58 | } 59 | 60 | done(); 61 | }); 62 | }, 63 | 64 | configuring: { 65 | files() { 66 | this.containerName = this.namingConventions.componentName.clean(this.containerName); 67 | 68 | this.files = [ 69 | 'index.js', 70 | 'test.js', 71 | ]; 72 | }, 73 | }, 74 | 75 | writing: { 76 | component() { 77 | this.composeWith('rn:component', { 78 | options: { 79 | componentName: this.containerName, 80 | isContainer: true, 81 | addReducer: this.addReducer, 82 | selectorName: this.selectorName, 83 | boilerplateName: this.boilerplateName, 84 | doNotGenerateTests: this.doNotGenerateTests, 85 | }, 86 | }, { 87 | local: require.resolve('../component'), 88 | }); 89 | }, 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /baker/generators/escodegen.js: -------------------------------------------------------------------------------- 1 | import esprima from 'esprima'; 2 | 3 | const escodegenOptions = { 4 | format: { 5 | indent: { 6 | style: ' ', 7 | base: 0, 8 | adjustMultilineComment: false, 9 | preserveBlankLines: true, 10 | }, 11 | newline: '\n', 12 | space: ' ', 13 | json: false, 14 | renumber: false, 15 | hexadecimal: false, 16 | quotes: 'single', 17 | escapeless: false, 18 | compact: false, 19 | parentheses: true, 20 | semicolons: true, 21 | safeConcatenation: false, 22 | }, 23 | moz: { 24 | starlessGenerator: false, 25 | parenthesizedComprehensionBlock: false, 26 | comprehensionExpressionStartsWithAssignment: false, 27 | }, 28 | parse: esprima.parse, 29 | comment: true, 30 | sourceMap: undefined, 31 | sourceMapRoot: null, 32 | sourceMapWithCode: false, 33 | file: undefined, 34 | // sourceContent: originalSource, 35 | directive: false, 36 | verbatim: undefined, 37 | }; 38 | 39 | export default escodegenOptions; 40 | -------------------------------------------------------------------------------- /baker/generators/esprima.js: -------------------------------------------------------------------------------- 1 | const esprimaOptions = { 2 | sourceType: 'module', 3 | comment: true, 4 | range: true, 5 | loc: true, 6 | tokens: true, 7 | raw: false, 8 | }; 9 | 10 | export default esprimaOptions; 11 | -------------------------------------------------------------------------------- /baker/generators/list/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | 3 | module.exports = BaseGenerator.extend({ 4 | constructor(...args) { 5 | BaseGenerator.apply(this, args); 6 | }, 7 | 8 | prompting() { 9 | const done = this.async(); 10 | const availableGenerators = [ 11 | { name: 'Component', value: 'component' }, 12 | { name: 'Container', value: 'container' }, 13 | { name: 'Navigation', value: 'navigation' }, 14 | { name: 'Saga', value: 'saga' }, 15 | { name: 'Model', value: 'model' }, 16 | ]; 17 | 18 | return this.prompt([{ 19 | type: 'list', 20 | choices: availableGenerators, 21 | name: 'generator', 22 | message: 'Choose the generator to use', 23 | default: availableGenerators[0].value, 24 | }], answers => { 25 | this.composeWith(`rn:${answers.generator}`, {}, 26 | { local: require.resolve(`../${answers.generator}`) } 27 | ); 28 | done(); 29 | }); 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /baker/generators/model/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | import changeCase from 'change-case'; 3 | 4 | module.exports = BaseGenerator.extend({ 5 | constructor(args, options) { 6 | BaseGenerator.call(this, args, options); 7 | 8 | this.modelName = options.modelName; 9 | }, 10 | 11 | prompting() { 12 | const prompts = []; 13 | 14 | if (!this.modelName) { 15 | prompts.push({ 16 | type: 'input', 17 | name: 'modelName', 18 | message: 'What should your model be called?', 19 | default: 'Todo', 20 | validate: value => this.namingConventions.modelName.regEx.test(value), 21 | }); 22 | } 23 | 24 | if (prompts.length !== 0) { 25 | const done = this.async(); 26 | this.prompt(prompts, answers => { 27 | if (!this.modelName) { 28 | this.modelName = answers.modelName; 29 | } 30 | done(); 31 | }); 32 | } 33 | }, 34 | 35 | writing: { 36 | serverModel() { 37 | this.template('server/models/index.js.hbs', 38 | `${this.serverDirectory}/models/${this.modelName}.js`); 39 | }, 40 | 41 | updateGraphQLSchemaFile() { 42 | const graphQLSchemaModulePath = `${this.serverDirectory}/graphql/schema.js`; 43 | let schemaModuleContent; 44 | let schemaModule; 45 | 46 | if (this._fileExists(this.destinationPath(graphQLSchemaModulePath))) { 47 | schemaModuleContent = this._readFile(graphQLSchemaModulePath); 48 | } else { 49 | schemaModuleContent = this.read(this.templatePath('schema.js.hbs')); 50 | } 51 | 52 | try { 53 | schemaModule = this.parseJSSource(schemaModuleContent); 54 | } catch (e) { 55 | const path = this.destinationPath(graphQLSchemaModulePath); 56 | this.env.error(`There seems to be an issue with your reducers module (${path})`, e); 57 | return; 58 | } 59 | 60 | // add import statement for the new model 61 | schemaModule.body = [{ 62 | type: 'ImportDeclaration', 63 | specifiers: [ 64 | { 65 | type: 'ImportDefaultSpecifier', 66 | local: { 67 | type: 'Identifier', 68 | name: `${this.modelName}`, 69 | }, 70 | imported: { 71 | type: 'Identifier', 72 | name: `${this.modelName}`, 73 | }, 74 | }, 75 | ], 76 | source: { 77 | type: 'Literal', 78 | value: `../models/${this.modelName}`, 79 | raw: `'../models/${this.modelName}'`, 80 | }, 81 | }, ...schemaModule.body]; 82 | 83 | // include schema of a newly created model in graphql/schema module 84 | const queriesDeclaration = schemaModule.body.find( 85 | i => i.type === 'VariableDeclaration' && i.declarations && 86 | i.declarations[0] && i.declarations[0].id.name === 'queries' 87 | ); 88 | 89 | if (!queriesDeclaration) { 90 | // eslint-disable-next-line max-len 91 | this.env.error(`Your ${this.serverDirectory}/graphql/schema.js module is missing queries const`); 92 | } 93 | 94 | queriesDeclaration.declarations[0].init.properties.push({ 95 | type: 'Property', 96 | key: { 97 | type: 'Identifier', 98 | name: changeCase.camelCase(this.modelName), 99 | }, 100 | computed: false, 101 | value: { 102 | type: 'MemberExpression', 103 | computed: false, 104 | object: { 105 | type: 'Identifier', 106 | name: this.modelName, 107 | }, 108 | property: { 109 | type: 'Identifier', 110 | name: 'RootQuery', 111 | }, 112 | }, 113 | kind: 'init', 114 | method: false, 115 | shorthand: false, 116 | }); 117 | 118 | // include mutations of a newly created model in graphql/schema module 119 | const mutationsDeclaration = schemaModule.body.find( 120 | i => i.type === 'VariableDeclaration' && i.declarations && 121 | i.declarations[0] && i.declarations[0].id.name === 'mutations' 122 | ); 123 | 124 | if (!mutationsDeclaration) { 125 | // eslint-disable-next-line max-len 126 | this.env.error(`Your ${this.serverDirectory}/graphql/schema.js module is missing mutations const`); 127 | } 128 | 129 | mutationsDeclaration.declarations[0].init.elements.push({ 130 | type: 'MemberExpression', 131 | computed: false, 132 | object: { 133 | type: 'Identifier', 134 | name: this.modelName, 135 | }, 136 | property: { 137 | type: 'Identifier', 138 | name: 'Mutations', 139 | }, 140 | }); 141 | 142 | try { 143 | this.generateJSFile(schemaModule, graphQLSchemaModulePath); 144 | } catch (e) { 145 | console.error(`error generating ${this.serverDirectory}/graphql/schema.js`, e); 146 | } 147 | }, 148 | }, 149 | }); 150 | -------------------------------------------------------------------------------- /baker/generators/model/templates/schema.js.hbs: -------------------------------------------------------------------------------- 1 | /* eslint comma-dangle: "off", prefer-template: "off", eol-last: "off" */ 2 | import { 3 | GraphQLObjectType, 4 | GraphQLSchema, 5 | } from 'graphql'; 6 | import assert from 'assert'; 7 | 8 | const queries = {}; 9 | const mutations = []; 10 | 11 | // XX: check for duplicate mutation declarations 12 | // accross different models 13 | function checkForDuplicates(listOfMutations) { 14 | const existingMutations = []; 15 | listOfMutations.forEach(ms => Object.keys(ms).forEach(m => { 16 | assert(existingMutations.indexOf(m) === -1, 'Duplicate mutation declaration:' + m); 17 | existingMutations.push(m); 18 | })); 19 | } 20 | checkForDuplicates(mutations); 21 | 22 | export default new GraphQLSchema({ 23 | query: new GraphQLObjectType({ 24 | name: 'Query', 25 | fields: queries 26 | }), 27 | mutation: new GraphQLObjectType({ 28 | name: 'Mutation', 29 | fields: Object.assign.apply(this, [ 30 | {}, 31 | ...mutations 32 | ]) 33 | }) 34 | }); 35 | -------------------------------------------------------------------------------- /baker/generators/model/templates/server/models/index.js.hbs: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/node'; 2 | import { 3 | GraphQLID, 4 | GraphQLObjectType, 5 | GraphQLString, 6 | GraphQLNonNull, 7 | GraphQLList, 8 | } from 'graphql'; 9 | 10 | 11 | const {{modelName}} = Parse.Object.extend('{{modelName}}'); 12 | 13 | const {{modelName}}Type = new GraphQLObjectType({ 14 | name: '{{modelName}}', 15 | description: 'A concise description of what {{modelName}} is', 16 | fields: () => ({ 17 | id: { 18 | type: GraphQLID, 19 | }, 20 | // XX: you should probably replace this with something 21 | // relevant to your model 22 | text: { 23 | type: GraphQLString, 24 | resolve: {{camelCase modelName}} => {{camelCase modelName}}.get('text'), 25 | }, 26 | // more field defs here 27 | }), 28 | }); 29 | 30 | {{modelName}}.SchemaType = {{modelName}}Type; 31 | 32 | {{modelName}}.RootQuery = { 33 | type: new GraphQLList({{modelName}}.SchemaType), 34 | resolve: (_, args, { Query }) => { 35 | const query = new Query({{modelName}}); 36 | return query.find(); 37 | }, 38 | }; 39 | 40 | {{modelName}}.Mutations = { 41 | add{{modelName}}: { 42 | type: {{modelName}}.SchemaType, 43 | description: 'Create a new instance of {{modelName}}', 44 | args: { 45 | text: { type: new GraphQLNonNull(GraphQLString) }, 46 | }, 47 | resolve: (_, { text }, { Query, user }) => { 48 | const {{camelCase modelName}} = new Query({{modelName}}).create({ text }); 49 | if (user) { 50 | {{camelCase modelName}}.setACL(new Parse.ACL(user)); 51 | } 52 | return {{camelCase modelName}}.save().then(td => td); 53 | }, 54 | }, 55 | delete{{modelName}}: { 56 | type: {{modelName}}.SchemaType, 57 | description: 'Delete an instance of {{modelName}}', 58 | args: { 59 | id: { type: new GraphQLNonNull(GraphQLID) }, 60 | }, 61 | resolve: (_, { id }, { Query }) => 62 | new Query({{modelName}}).get(id).then(({{camelCase modelName}}) => { 63 | if ({{camelCase modelName}}) { 64 | return {{camelCase modelName}}.destroy(); 65 | } 66 | return {{camelCase modelName}}; 67 | }), 68 | }, 69 | }; 70 | 71 | export default {{modelName}}; 72 | -------------------------------------------------------------------------------- /baker/generators/naming.js: -------------------------------------------------------------------------------- 1 | import changeCase from 'change-case'; 2 | 3 | const namingConventions = { 4 | componentName: { 5 | regEx: /^[A-Z][0-9A-Z]*$/i, 6 | clean: name => changeCase.pascal(name), 7 | }, 8 | 9 | reducerName: { 10 | clean: name => changeCase.camelCase(name), 11 | }, 12 | 13 | selectorName: { 14 | clean: componentName => `select${changeCase.pascalCase(componentName)}`, 15 | }, 16 | 17 | sagaName: { 18 | regEx: /^[A-Z][0-9A-Z]*$/i, 19 | clean: name => changeCase.camelCase(name), 20 | }, 21 | 22 | modelName: { 23 | regEx: /^[A-Z][0-9A-Z]*$/i, 24 | clean: name => changeCase.pascal(name), 25 | }, 26 | }; 27 | 28 | export default namingConventions; 29 | -------------------------------------------------------------------------------- /baker/generators/navigation/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | 3 | module.exports = BaseGenerator.extend({ 4 | prompting() { 5 | const done = this.async(); 6 | const availableBoilerplates = this.navigationBoilerplates; 7 | const prompts = [ 8 | { 9 | type: 'input', 10 | name: 'componentName', 11 | message: 'What should your container be called?', 12 | default: 'Navigation', 13 | validate: value => this.namingConventions.componentName.regEx.test(value), 14 | }, 15 | { 16 | type: 'list', 17 | name: 'boilerplateName', 18 | message: 'Which boilerplate do you want to use?', 19 | default: availableBoilerplates[0], 20 | choices: availableBoilerplates, 21 | }, 22 | ]; 23 | 24 | this.prompt(prompts, answers => { 25 | this.componentName = answers.componentName; 26 | this.boilerplateName = answers.boilerplateName; 27 | done(); 28 | }); 29 | }, 30 | 31 | writing: { 32 | container() { 33 | this.composeWith('rn:container', { 34 | options: { 35 | name: this.componentName, 36 | isContainer: true, 37 | addReducer: true, 38 | boilerplateName: this.boilerplateName, 39 | doNotGenerateTests: true, 40 | }, 41 | }, { 42 | local: require.resolve('../container'), 43 | }); 44 | }, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /baker/generators/reducer/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | import _ from 'lodash'; 3 | 4 | module.exports = BaseGenerator.extend({ 5 | constructor(args, options) { 6 | BaseGenerator.call(this, args, options); 7 | 8 | if (!options || !options.container) { 9 | // XX: reducer generator is currently only meant 10 | // to be used through composeWith 11 | this.env.error('container option is required in reducer generator'); 12 | return; 13 | } 14 | 15 | this.container = options.container; 16 | this.reducerName = this.namingConventions.reducerName.clean(this.container); 17 | this.boilerplateName = options.boilerplateName || 'Vanila'; 18 | this.doNotGenerateTests = options.doNotGenerateTests; 19 | }, 20 | 21 | configuring: { 22 | files() { 23 | this.files = [ 24 | 'actions.js.hbs', 25 | 'constants.js.hbs', 26 | 'reducer.js.hbs', 27 | ]; 28 | 29 | if (!this.doNotGenerateTests) { 30 | this.files = [ 31 | 'actions.test.js.hbs', 32 | 'reducer.test.js.hbs', 33 | ...this.files, 34 | ]; 35 | } 36 | }, 37 | 38 | boilerplate() { 39 | this.boilerplate = this._renderBoilerplate(this.boilerplateName); 40 | }, 41 | }, 42 | 43 | writing: { 44 | everything() { 45 | this.runBoilerplateBeforeHook(this.boilerplateName); 46 | 47 | this.files.forEach(f => this.template(f, 48 | `${this.appDirectory}/components/${this.container}/${this._dropHBSExtension(f)}`)); 49 | 50 | this.runBoilerplateAfterHook(this.boilerplateName); 51 | }, 52 | 53 | updateRootReducersModule() { 54 | const reducersModulePath = `${this.appDirectory}/reducers.js`; 55 | let reducersModuleContent; 56 | let reducersModule; 57 | 58 | if (this._fileExists(this.destinationPath(reducersModulePath))) { 59 | reducersModuleContent = this._readFile(reducersModulePath); 60 | } else { 61 | reducersModuleContent = this.read(this.templatePath('reducers.js.hbs')); 62 | } 63 | 64 | try { 65 | // reducersModule = esprima.parse(reducersModuleContent, this.esprimaOptions); 66 | reducersModule = this.parseJSSource(reducersModuleContent); 67 | } catch (e) { 68 | const path = this.destinationPath(reducersModulePath); 69 | this.env.error(`There seems to be an issue with your reducers module (${path})`, e); 70 | return; 71 | } 72 | 73 | // add import statement to the top of the 74 | // reducers module including new reducer 75 | reducersModule.body = [{ 76 | type: 'ImportDeclaration', 77 | specifiers: [ 78 | { 79 | type: 'ImportDefaultSpecifier', 80 | local: { 81 | type: 'Identifier', 82 | name: `${this.reducerName}`, 83 | }, 84 | imported: { 85 | type: 'Identifier', 86 | name: `${this.reducerName}`, 87 | }, 88 | }, 89 | ], 90 | source: { 91 | type: 'Literal', 92 | value: `./components/${this.container}/reducer`, 93 | raw: `'./components/${this.container}/reducer'`, 94 | }, 95 | }, ...reducersModule.body]; 96 | 97 | // add new reducer to the module export 98 | // find top level var called applicationReducers 99 | // add new reducer to init.properties 100 | 101 | const applicationReducersVar = _.find(reducersModule.body, 102 | d => d.type === 'VariableDeclaration' && d.declarations[0].id.name === 'applicationReducers' 103 | ); 104 | 105 | if (applicationReducersVar) { 106 | applicationReducersVar.declarations[0].init.properties.push({ 107 | type: 'Property', 108 | key: { 109 | type: 'Identifier', 110 | name: this.reducerName, 111 | }, 112 | computed: false, 113 | value: { 114 | type: 'Identifier', 115 | name: this.reducerName, 116 | }, 117 | kind: 'init', 118 | method: false, 119 | shorthand: false, 120 | }); 121 | } else { 122 | // XX: this should not happen normally 123 | // unless applicationReducers got moved somewhere, deleted 124 | this.env.error('Your reducers.js module is missing applicationReducers var'); 125 | return; 126 | } 127 | 128 | try { 129 | this.generateJSFile(reducersModule, reducersModulePath); 130 | } catch (e) { 131 | console.error('error generating reducers.js', e); 132 | } 133 | }, 134 | }, 135 | }); 136 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/actions.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{container}} actions 4 | * 5 | */ 6 | 7 | import { 8 | DEFAULT_ACTION, 9 | } from './constants'; 10 | 11 | export function defaultAction() { 12 | return { 13 | type: DEFAULT_ACTION, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/actions.test.js.hbs: -------------------------------------------------------------------------------- 1 | import { defaultAction } from './actions'; 2 | import { DEFAULT_ACTION } from './constants'; 3 | 4 | describe('{{container}} actions', () => { 5 | it('creates a default action', () => { 6 | const expectedAction = { 7 | type: DEFAULT_ACTION, 8 | }; 9 | expect(defaultAction()).to.eql(expectedAction); 10 | }); 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/boilerplates/Vanila.js.hbs: -------------------------------------------------------------------------------- 1 | import { DEFAULT_ACTION } from './constants'; 2 | 3 | function {{reducerName}}(state = {}, action) { 4 | switch (action.type) { 5 | case DEFAULT_ACTION: 6 | return state; 7 | default: 8 | return state; 9 | } 10 | } 11 | 12 | export default {{reducerName}}; 13 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/boilerplates/navigation/Cards.js.hbs: -------------------------------------------------------------------------------- 1 | import { cardStackReducer } from 'react-native-navigation-redux-helpers'; 2 | 3 | const initialState = { 4 | key: '{{reducerName}}', 5 | index: 0, 6 | routes: [ 7 | { 8 | key: '{{reducerName}}-initial-card', 9 | index: 0, 10 | }, 11 | ], 12 | }; 13 | 14 | const {{reducerName}} = cardStackReducer(initialState); 15 | 16 | export default {{reducerName}}; 17 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/boilerplates/navigation/Tabs.js.hbs: -------------------------------------------------------------------------------- 1 | import { tabReducer } from 'react-native-navigation-redux-helpers'; 2 | 3 | const simpleIcon = { 4 | // eslint-disable-next-line max-len 5 | uri: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAQAAACSR7JhAAADtUlEQVR4Ac3YA2Bj6QLH0XPT1Fzbtm29tW3btm3bfLZtv7e2ObZnms7d8Uw098tuetPzrxv8wiISrtVudrG2JXQZ4VOv+qUfmqCGGl1mqLhoA52oZlb0mrjsnhKpgeUNEs91Z0pd1kvihA3ULGVHiQO2narKSHKkEMulm9VgUyE60s1aWoMQUbpZOWE+kaqs4eLEjdIlZTcFZB0ndc1+lhB1lZrIuk5P2aib1NBpZaL+JaOGIt0ls47SKzLC7CqrlGF6RZ09HGoNy1lYl2aRSWL5GuzqWU1KafRdoRp0iOQEiDzgZPnG6DbldcomadViflnl/cL93tOoVbsOLVM2jylvdWjXolWX1hmfZbGR/wjypDjFLSZIRov09BgYmtUqPQPlQrPapecLgTIy0jMgPKtTeob2zWtrGH3xvjUkPCtNg/tm1rjwrMa+mdUkPd3hWbH0jArPGiU9ufCsNNWFZ40wpwn+62/66R2RUtoso1OB34tnLOcy7YB1fUdc9e0q3yru8PGM773vXsuZ5YIZX+5xmHwHGVvlrGPN6ZSiP1smOsMMde40wKv2VmwPPVXNut4sVpUreZiLBHi0qln/VQeI/LTMYXpsJtFiclUN+5HVZazim+Ky+7sAvxWnvjXrJFneVtLWLyPJu9K3cXLWeOlbMTlrIelbMDlrLenrjEQOtIF+fuI9xRp9ZBFp6+b6WT8RrxEpdK64BuvHgDk+vUy+b5hYk6zfyfs051gRoNO1usU12WWRWL73/MMEy9pMi9qIrR4ZpV16Rrvduxazmy1FSvuFXRkqTnE7m2kdb5U8xGjLw/spRr1uTov4uOgQE+0N/DvFrG/Jt7i/FzwxbA9kDanhf2w+t4V97G8lrT7wc08aA2QNUkuTfW/KimT01wdlfK4yEw030VfT0RtZbzjeMprNq8m8tnSTASrTLti64oBNdpmMQm0eEwvfPwRbUBywG5TzjPCsdwk3IeAXjQblLCoXnDVeoAz6SfJNk5TTzytCNZk/POtTSV40NwOFWzw86wNJRpubpXsn60NJFlHeqlYRbslqZm2jnEZ3qcSKgm0kTli3zZVS7y/iivZTweYXJ26Y+RTbV1zh3hYkgyFGSTKPfRVbRqWWVReaxYeSLarYv1Qqsmh1s95S7G+eEWK0f3jYKTbV6bOwepjfhtafsvUsqrQvrGC8YhmnO9cSCk3yuY984F1vesdHYhWJ5FvASlacshUsajFt2mUM9pqzvKGcyNJW0arTKN1GGGzQlH0tXwLDgQTurS8eIQAAAABJRU5ErkJggg==', 6 | scale: 3, 7 | }; 8 | 9 | const tabsDefinitions = { 10 | routes: [ 11 | { key: 'tab1', icon: simpleIcon, title: 'Tab 1' }, 12 | { key: 'tab2', icon: simpleIcon, title: 'Tab 2' }, 13 | { key: 'tab3', icon: simpleIcon, title: 'Tab 3' }, 14 | ], 15 | key: 'ApplicationTabs', 16 | index: 0, 17 | }; 18 | 19 | const {{reducerName}} = tabReducer(tabsDefinitions); 20 | 21 | export default {{reducerName}}; -------------------------------------------------------------------------------- /baker/generators/reducer/templates/constants.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{container}} constants 4 | * 5 | */ 6 | 7 | export const DEFAULT_ACTION = '{{appDirectory}}/{{container}}/DEFAULT_ACTION'; 8 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/reducer.js.hbs: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * {{container}} reducer 4 | * 5 | */ 6 | 7 | {{{boilerplate}}} 8 | 9 | export function select{{pascalCase reducerName}}(state) { 10 | return state.get('{{reducerName}}'); 11 | } 12 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/reducer.test.js.hbs: -------------------------------------------------------------------------------- 1 | import reducer from './reducer'; 2 | 3 | describe('{{container}} reducer', () => { 4 | it('returns default state', () => { 5 | expect( 6 | reducer(undefined, {}) 7 | ).to.eql({}); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /baker/generators/reducer/templates/reducers.js.hbs: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutable'; 2 | 3 | // XX: Do not rename this variable if you want reducer generator 4 | // to keep working properly (and you do want that, right?) 5 | const applicationReducers = { 6 | // XX: you can kill this reducer once you have at least 1 proper reducer in the system 7 | dummy: (state = {}, action) => state 8 | }; 9 | 10 | export default function createReducer() { 11 | return combineReducers(applicationReducers); 12 | }; 13 | -------------------------------------------------------------------------------- /baker/generators/saga/index.js: -------------------------------------------------------------------------------- 1 | import BaseGenerator from '../base'; 2 | import _ from 'lodash'; 3 | 4 | module.exports = BaseGenerator.extend({ 5 | constructor(args, options) { 6 | BaseGenerator.call(this, args, options); 7 | 8 | this.boilerplateName = options.boilerplateName; 9 | }, 10 | 11 | prompting() { 12 | const done = this.async(); 13 | const prompts = [{ 14 | type: 'input', 15 | name: 'sagaName', 16 | message: 'What should your saga be called?', 17 | default: 'talkToServer', 18 | validate: value => this.namingConventions.sagaName.regEx.test(value), 19 | }]; 20 | 21 | if (!this.boilerplateName) { 22 | prompts.push({ 23 | type: 'list', 24 | name: 'boilerplateName', 25 | message: 'Which boilerplate do you want to use?', 26 | default: 'Vanila', 27 | choices: () => this._listAvailableBoilerPlates(), 28 | }); 29 | } 30 | 31 | this.prompt(prompts, answers => { 32 | this.sagaName = this.namingConventions.sagaName.clean(answers.sagaName); 33 | 34 | if (typeof this.boilerplateName === 'undefined') { 35 | this.boilerplateName = answers.boilerplateName; 36 | } 37 | 38 | done(); 39 | }); 40 | }, 41 | 42 | writing: { 43 | sagaFile() { 44 | this.runBoilerplateBeforeHook(this.boilerplateName); 45 | 46 | this.template('saga.js.hbs', `${this.appDirectory}/sagas/${this.sagaName}.js`, 47 | Object.assign(this, { 48 | boilerplate: this._renderBoilerplate(this.boilerplateName), 49 | }) 50 | ); 51 | 52 | this.runBoilerplateAfterHook(this.boilerplateName); 53 | }, 54 | 55 | updateSagasIndex() { 56 | const sagasIndex = this.destinationPath(`${this.appDirectory}/sagas/index.js`); 57 | let sagasIndexContent; 58 | let sagasModule; 59 | 60 | if (this._fileExists(sagasIndex)) { 61 | sagasIndexContent = this._readFile(sagasIndex); 62 | } else { 63 | sagasIndexContent = this._readFile(this.templatePath('index.js')); 64 | } 65 | 66 | try { 67 | sagasModule = this.parseJSSource(sagasIndexContent); 68 | } catch (e) { 69 | console.error('error is', e); 70 | this.env.error(`There seems to be an issue with your sagas module (${sagasIndex})`, e); 71 | return; 72 | } 73 | 74 | // add import statement to the top of the 75 | // sagas index module to include new saga 76 | sagasModule.body = [{ 77 | type: 'ImportDeclaration', 78 | specifiers: [ 79 | { 80 | type: 'ImportSpecifier', 81 | local: { 82 | type: 'Identifier', 83 | name: this.sagaName, 84 | }, 85 | imported: { 86 | type: 'Identifier', 87 | name: this.sagaName, 88 | }, 89 | }, 90 | ], 91 | source: { 92 | type: 'Literal', 93 | value: `./${this.sagaName}`, 94 | raw: `'./${this.sagaName}'`, 95 | }, 96 | }, ...sagasModule.body]; 97 | 98 | const sagasList = _.find(sagasModule.body, d => 99 | d.type === 'VariableDeclaration' && d.declarations[0].id.name === 'sagas' 100 | ); 101 | 102 | if (!sagasList) { 103 | this.env.error( 104 | // eslint-disable-next-line max-len 105 | `There seems to be an issue with your sagas module (${sagasIndex}) - cannot find list of sagas` 106 | ); 107 | return; 108 | } 109 | 110 | sagasList.declarations[0].init.elements.push({ 111 | type: 'Identifier', 112 | name: this.sagaName, 113 | }); 114 | 115 | try { 116 | this.generateJSFile(sagasModule, sagasIndex); 117 | } catch (e) { 118 | console.error('error generating sagas/index.js', e); 119 | } 120 | }, 121 | }, 122 | }); 123 | -------------------------------------------------------------------------------- /baker/generators/saga/templates/boilerplates/MethodCall.js.hbs: -------------------------------------------------------------------------------- 1 | import { takeEvery } from 'redux-saga'; 2 | import { put, call } from 'redux-saga/effects'; 3 | import { apiCall } from '../api/apiCall'; 4 | 5 | // XX: you will probably move these constants into constants module 6 | // associated with an appropriate reducer 7 | const {{constantCase sagaName}}_REQUEST = 'sagas/{{constantCase sagaName}}_REQUEST'; 8 | const {{constantCase sagaName}}_ERROR = 'sagas/{{constantCase sagaName}}_ERROR'; 9 | const {{constantCase sagaName}}_SUCCESS = 'sagas/{{constantCase sagaName}}_SUCCESS'; 10 | 11 | function* run{{pascalCase sagaName}}(action) { 12 | try { 13 | const response = yield call(apiCall); 14 | yield put({ 15 | type: {{constantCase sagaName}}_SUCCESS, 16 | payload: { 17 | // data from the result of API call 18 | }, 19 | }); 20 | } catch (error) { 21 | yield put({ 22 | type: {{constantCase sagaName}}_ERROR, 23 | payload: { 24 | error, 25 | }, 26 | }); 27 | } 28 | } 29 | 30 | export function* {{sagaName}}() { 31 | yield* takeEvery({{constantCase sagaName}}_REQUEST, run{{pascalCase sagaName}}); 32 | } 33 | -------------------------------------------------------------------------------- /baker/generators/saga/templates/boilerplates/Vanila.js.hbs: -------------------------------------------------------------------------------- 1 | import { takeEvery } from 'redux-saga'; 2 | import { put } from 'redux-saga/effects'; 3 | 4 | function* run{{pascalCase sagaName}}(action) { 5 | console.log('processeing action', action); 6 | yield put({ 7 | type: 'MESSAGE_TO_SEND', 8 | payload: { 9 | }, 10 | }); 11 | } 12 | 13 | export function* {{sagaName}}() { 14 | yield* takeEvery('NAME_OF_THE_MESSAGE', run{{pascalCase sagaName}}); 15 | } 16 | -------------------------------------------------------------------------------- /baker/generators/saga/templates/index.js: -------------------------------------------------------------------------------- 1 | // XX: this is used by the code generator 2 | // please do not rename this 3 | const sagas = [ 4 | ]; 5 | 6 | module.exports = sagas; 7 | -------------------------------------------------------------------------------- /baker/generators/saga/templates/saga.js.hbs: -------------------------------------------------------------------------------- 1 | {{{boilerplate}}} -------------------------------------------------------------------------------- /baker/generators/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-vars": 0, 7 | "no-unused-expressions":0, 8 | "max-len": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /baker/generators/test/setup.js: -------------------------------------------------------------------------------- 1 | require('babel-register')({ 2 | presets: ['es2015'], 3 | }); 4 | -------------------------------------------------------------------------------- /baker/generators/test/tests/app.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import assert from 'yeoman-assert'; 3 | import helpers from 'yeoman-test'; 4 | import chai from 'chai'; 5 | import sinon from 'sinon'; 6 | import sinonChai from 'sinon-chai'; 7 | import fsExtra from 'fs-extra'; 8 | import fs from 'fs'; 9 | import _ from 'lodash'; 10 | 11 | const expect = chai.expect; 12 | const appGeneratorModule = path.join(__dirname, '../../app'); 13 | 14 | describe('generator-rn:app', () => { 15 | let _generator; 16 | let _checkIfRNIsInstalledStub = null; 17 | let _initRNSpy = null; 18 | let _abortSetupStub = null; 19 | const applicationName = 'MyReactApp'; 20 | const applicationFiles = [ 21 | 'app/reducers.js', 22 | 'app/setup.js', 23 | 'app/store.js', 24 | 'app/tests.js', 25 | 'app/components/App/index.js', 26 | 'app/components/App/styles.js', 27 | 'app/sagas/index.js', 28 | 'index.ios.js', 29 | 'index.android.js', 30 | 'package.json', 31 | ]; 32 | 33 | const _stubThings = generator => { 34 | _generator = generator; 35 | _checkIfRNIsInstalledStub = sinon.stub(generator, '_checkIfRNIsInstalled').returns(true); 36 | _initRNSpy = sinon.stub(generator, '_initRN').returns(true); 37 | _abortSetupStub = sinon.stub(generator, '_abortSetup').returns(true); 38 | }; 39 | 40 | const _unstubThings = () => { 41 | _checkIfRNIsInstalledStub && _checkIfRNIsInstalledStub.restore(); 42 | _initRNSpy && _initRNSpy.restore(); 43 | _abortSetupStub && _abortSetupStub.restore(); 44 | }; 45 | 46 | describe('simple generator', () => { 47 | before(done => { 48 | helpers.run(appGeneratorModule) 49 | .on('ready', _stubThings) 50 | .on('end', done); 51 | }); 52 | 53 | after(_unstubThings); 54 | 55 | it('checks if react-native is installed', () => { 56 | expect(_checkIfRNIsInstalledStub.calledOnce).to.be.ok; 57 | }); 58 | 59 | it('runs RN setup script', () => { 60 | expect(_initRNSpy.calledOnce).to.be.ok; 61 | }); 62 | 63 | it('sets up all the app files', () => { 64 | assert.file(applicationFiles); 65 | }); 66 | }); 67 | 68 | describe('running generator in a non-empty directory', () => { 69 | before(done => { 70 | helpers.run(appGeneratorModule) 71 | .inTmpDir(dir => { 72 | fsExtra.copySync( 73 | path.join(__dirname, './fixtures/random-file.txt'), 74 | path.join(dir, 'random-file.txt') 75 | ); 76 | }) 77 | .withPrompts({ 78 | name: applicationName, 79 | }) 80 | .on('ready', _stubThings) 81 | .on('end', done); 82 | }); 83 | 84 | after(_unstubThings); 85 | 86 | it('sets things up in a newly created directory', () => { 87 | expect(_generator.destinationPath('.').indexOf(applicationName) !== -1).to.be.ok; 88 | assert.file(applicationFiles); 89 | }); 90 | }); 91 | 92 | describe('running generator in a non-empty directory with something that looks like a RN app', () => { 93 | before(done => { 94 | helpers.run(appGeneratorModule) 95 | .inTmpDir(dir => { 96 | // XX: make it look like a directory with some RN artifacts 97 | fs.mkdirSync(path.join(dir, 'android')); 98 | fs.mkdirSync(path.join(dir, 'ios')); 99 | fs.writeFileSync(path.join(dir, 'index.ios.js'), '00000000'); 100 | fs.writeFileSync(path.join(dir, 'index.android.js'), '00000000'); 101 | }) 102 | .withPrompts({ 103 | name: applicationName, 104 | }) 105 | .on('ready', _stubThings) 106 | .on('end', done); 107 | }); 108 | 109 | after(_unstubThings); 110 | 111 | it('bails on app generation', () => { 112 | expect(_abortSetupStub.calledOnce).to.be.ok; 113 | }); 114 | }); 115 | 116 | describe('running generator in a non-empty directory with --baker flag', () => { 117 | let originalPackageJSON; 118 | 119 | before(done => { 120 | helpers.run(appGeneratorModule) 121 | .inTmpDir(dir => { 122 | fsExtra.copySync( 123 | path.join(__dirname, './fixtures/random-file.txt'), 124 | path.join(dir, 'random-file.txt') 125 | ); 126 | fsExtra.copySync( 127 | path.join(__dirname, './fixtures/package.json'), 128 | path.join(dir, 'package.json') 129 | ); 130 | originalPackageJSON = fsExtra.readJsonSync(path.join(__dirname, './fixtures/package.json')); 131 | }) 132 | .withOptions({ baker: 'baker' }) 133 | .withPrompts({ 134 | name: applicationName, 135 | }) 136 | .on('ready', _stubThings) 137 | .on('end', done); 138 | }); 139 | 140 | after(_unstubThings); 141 | 142 | it('does not create a new directory', () => { 143 | expect(_generator.destinationPath('.').indexOf(applicationName) === -1).to.be.ok; 144 | }); 145 | 146 | it('sets up all the application files', () => { 147 | assert.file(applicationFiles); 148 | }); 149 | 150 | it('updates existing package.json with relevant data but also keeps original jazz in deps and scripts', () => { 151 | const packageJSON = fsExtra.readJsonSync(_generator.destinationPath('package.json')); 152 | 153 | expect(packageJSON.devDependencies).to.contain.all.keys(originalPackageJSON.devDependencies); 154 | expect(packageJSON.scripts).to.contain.all.keys(originalPackageJSON.scripts); 155 | 156 | expect(packageJSON.dependencies).to.contain.all.keys([ 157 | 'react-redux', 158 | 'redux', 159 | 'redux-immutable', 160 | 'redux-saga', 161 | 'reselect', 162 | ]); 163 | 164 | expect(packageJSON.devDependencies).to.contain.all.keys([ 165 | 'react-dom', 166 | 'react-native-mock', 167 | 'enzyme', 168 | ]); 169 | 170 | expect(packageJSON.name).to.equal(applicationName); 171 | }); 172 | 173 | it('adds test:app script to package.json', () => { 174 | const packageJSON = fsExtra.readJsonSync(_generator.destinationPath('package.json')); 175 | expect(packageJSON.scripts).to.contain.all.keys([ 176 | 'test:app', 177 | ]); 178 | }); 179 | }); 180 | 181 | describe('app with server setup', () => { 182 | before(done => { 183 | helpers.run(appGeneratorModule) 184 | .withPrompts({ 185 | name: applicationName, 186 | addServer: true, 187 | }) 188 | .on('ready', _stubThings) 189 | .on('end', done); 190 | }); 191 | 192 | after(_unstubThings); 193 | 194 | it('adds server folder with all the setup', () => { 195 | assert.file([ 196 | 'server/index.js', 197 | 'server/graphql/index.js', 198 | 'server/graphql/schema.js', 199 | 'server/models/Example.js', 200 | 'server/parse-server/index.js', 201 | 'server/public/images/logo.png', 202 | ]); 203 | }); 204 | 205 | it('adds server deps to package.json', () => { 206 | const packageJSON = fsExtra.readJsonSync(_generator.destinationPath('package.json')); 207 | expect(packageJSON.dependencies).to.contain.all.keys([ 208 | 'express', 209 | 'graphql', 210 | 'parse', 211 | 'parse-dashboard', 212 | 'parse-graphql-client', 213 | 'parse-graphql-server', 214 | 'parse-server', 215 | ]); 216 | 217 | expect(packageJSON.devDependencies).to.contain.all.keys([ 218 | 'mongodb-runner', 219 | 'babel-watch', 220 | ]); 221 | }); 222 | 223 | it('adds server related commands to package.json', () => { 224 | const packageJSON = fsExtra.readJsonSync(_generator.destinationPath('package.json')); 225 | expect(packageJSON.scripts).to.contain.all.keys([ 226 | 'mongo', 227 | 'server', 228 | 'server-watch', 229 | 'server-debug', 230 | ]); 231 | }); 232 | 233 | it('adds settings folder to the project with dev settings', () => { 234 | assert.file([ 235 | 'settings/development.json', 236 | 'settings/development.ios.json', 237 | 'settings/development.android.json', 238 | ]); 239 | }); 240 | 241 | it('adds settings.js to the app directory', () => { 242 | assert.file([ 243 | 'app/settings.js', 244 | ]); 245 | }); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /baker/generators/test/tests/base.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0, no-unused-expressions:0 */ 2 | 3 | import path from 'path'; 4 | import assert from 'yeoman-assert'; 5 | import helpers from 'yeoman-test'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | 10 | const expect = chai.expect; 11 | 12 | describe('base generator', () => { 13 | let _generator = null; 14 | 15 | before(done => { 16 | helpers.run(path.join(__dirname, '../../base.js')) 17 | .on('ready', generator => { _generator = generator; }) 18 | .on('end', done); 19 | }); 20 | 21 | it('is defined', () => { 22 | expect(_generator).to.be.ok; 23 | }); 24 | 25 | it('has appDirectory attribute', () => { 26 | expect(_generator.appDirectory).to.be.ok; 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /baker/generators/test/tests/component.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0, no-unused-expressions:0 */ 2 | 3 | import path from 'path'; 4 | import assert from 'yeoman-assert'; 5 | import helpers from 'yeoman-test'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import yeoman from 'yeoman-generator'; 10 | import mockery from 'mockery'; 11 | 12 | const expect = chai.expect; 13 | const componentGeneratorModule = path.join(__dirname, '../../component'); 14 | 15 | describe('generator-rn:component', () => { 16 | const componentName = 'MyComponent'; 17 | const boilerplate = 'Vanila'; 18 | const appDirectory = 'app'; 19 | const componentModule = `${appDirectory}/src/components/${componentName}/index.js`; 20 | const stylesheetModule = `${appDirectory}/src/components/${componentName}/styles.js`; 21 | 22 | let runBoilerplateBeforeHookSpy; 23 | let runBoilerplateAfterHookSpy; 24 | 25 | 26 | describe('simple component', () => { 27 | before(done => { 28 | runBoilerplateBeforeHookSpy = sinon.spy(); 29 | runBoilerplateAfterHookSpy = sinon.spy(); 30 | 31 | mockery.enable(); 32 | mockery.warnOnUnregistered(false); 33 | mockery.registerMock('./boilerplates', { 34 | runBoilerplateBeforeHook: runBoilerplateBeforeHookSpy, 35 | runBoilerplateAfterHook: runBoilerplateAfterHookSpy, 36 | }); 37 | 38 | helpers.run(componentGeneratorModule) 39 | .withPrompts({ 40 | componentName, 41 | boilerplateName: boilerplate, 42 | }).on('end', done); 43 | }); 44 | 45 | after(() => { 46 | mockery.deregisterAll(); 47 | mockery.disable(); 48 | }); 49 | 50 | it('sets up all component jazz', () => { 51 | assert.file([ 52 | 'index.js', 53 | 'styles.js', 54 | ].map(f => `${appDirectory}/src/components/${componentName}/${f}`)); 55 | }); 56 | 57 | it('exports component as-is without container wrapping', () => { 58 | assert.fileContent(componentModule, `export default ${componentName}`); 59 | }); 60 | 61 | it('generates a stylesheet', () => { 62 | assert.file(stylesheetModule); 63 | }); 64 | 65 | it('includes reference to the stylesheet', () => { 66 | assert.fileContent(componentModule, 'import styles from \'./styles\';'); 67 | }); 68 | 69 | it('calls boilerplate hooks', () => { 70 | expect(runBoilerplateBeforeHookSpy.calledOnce).to.be.ok; 71 | expect(runBoilerplateBeforeHookSpy.calledWith(boilerplate)).to.be.ok; 72 | expect(runBoilerplateAfterHookSpy.calledOnce).to.be.ok; 73 | expect(runBoilerplateAfterHookSpy.calledWith(boilerplate)).to.be.ok; 74 | }); 75 | 76 | it('creates component test file', () => { 77 | assert.file([ 78 | `${appDirectory}/src/components/${componentName}/index.test.js`, 79 | ]); 80 | }); 81 | }); 82 | 83 | describe('platform specific component', () => { 84 | before(done => { 85 | helpers.run(componentGeneratorModule) 86 | .withPrompts({ 87 | componentName, 88 | boilerplateName: boilerplate, 89 | platformSpecific: true, 90 | }).on('end', done); 91 | }); 92 | 93 | it('sets up .ios and .android versions of the component', () => { 94 | assert.file([ 95 | 'index.ios.js', 96 | 'index.android.js', 97 | ].map(f => `${appDirectory}/src/components/${componentName}/${f}`)); 98 | }); 99 | 100 | it('sets up .ios and .android versions of component tests', () => { 101 | const componentDirectory = `${appDirectory}/src/components/${componentName}`; 102 | 103 | assert.noFile(`${componentDirectory}/index.test.js`); 104 | assert.file([ 105 | `${componentDirectory}/index.android.test.js`, 106 | `${componentDirectory}/index.ios.test.js`, 107 | ]); 108 | }); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /baker/generators/test/tests/container.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0, no-unused-expressions:0 */ 2 | 3 | import path from 'path'; 4 | import assert from 'yeoman-assert'; 5 | import helpers from 'yeoman-test'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import yeoman from 'yeoman-generator'; 10 | 11 | const expect = chai.expect; 12 | const containerGeneratorModule = path.join(__dirname, '../../container'); 13 | 14 | describe('generator-rn:container', () => { 15 | const containerName = 'MyContainer'; 16 | const boilerplate = 'Vanila'; 17 | const appDirectory = 'app'; 18 | const containerModule = `${appDirectory}/src/components/${containerName}/index.js`; 19 | const stylesheetModule = `${appDirectory}/src/components/${containerName}/styles.js`; 20 | 21 | describe('simple container', () => { 22 | before(done => { 23 | helpers.run(containerGeneratorModule) 24 | .withPrompts({ 25 | containerName, 26 | boilerplateName: boilerplate, 27 | addReducer: false, 28 | }).on('end', done); 29 | }); 30 | 31 | it('sets up all container jazz', () => { 32 | assert.file([ 33 | 'index.js', 34 | 'index.test.js', 35 | ].map(f => `${appDirectory}/src/components/${containerName}/${f}`)); 36 | 37 | assert.noFile([ 38 | 'actions.js', 39 | 'actions.test.js', 40 | 'constants.js', 41 | 'reducer.js', 42 | 'reducer.test.js', 43 | ].map(f => `${appDirectory}/src/components/${containerName}/${f}`)); 44 | }); 45 | 46 | it('exposes component wrapped into connect and original component', () => { 47 | assert.fileContent(containerModule, 48 | `export default connect(mapStateToProps, mapDispatchToProps)(${containerName});`); 49 | assert.fileContent(containerModule, 50 | `export class ${containerName}` 51 | ); 52 | }); 53 | 54 | it('generates a stylesheet', () => { 55 | assert.file(stylesheetModule); 56 | }); 57 | 58 | it('includes reference to the stylesheet', () => { 59 | assert.fileContent(containerModule, 'import styles from \'./styles\';'); 60 | }); 61 | }); 62 | 63 | describe('container with a reducer', () => { 64 | before(done => { 65 | helpers.run(containerGeneratorModule) 66 | .withOptions({ 67 | boilerplateName: boilerplate, 68 | }) 69 | .withPrompts({ 70 | containerName, 71 | addReducer: true, 72 | }) 73 | .on('end', done); 74 | }); 75 | 76 | it('generates reducer related files', () => { 77 | assert.file([ 78 | 'actions.js', 79 | 'actions.test.js', 80 | 'constants.js', 81 | 'reducer.js', 82 | 'reducer.test.js', 83 | ].map(f => `${appDirectory}/src/components/${containerName}/${f}`)); 84 | }); 85 | 86 | it('imports selector from the reducer module', () => { 87 | assert.fileContent(`${appDirectory}/src/components/${containerName}/index.js`, 88 | 'import { selectMyContainer } from \'./reducer\';' 89 | ); 90 | }); 91 | 92 | it('references imported reducer in the connect set up', () => { 93 | assert.fileContent(`${appDirectory}/src/components/${containerName}/index.js`, 94 | 'createSelector(selectMyContainer, (myContainer) => ({ myContainer }))' 95 | ); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /baker/generators/test/tests/fixtures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baker", 3 | "version": "1.0.0", 4 | "description": "React Native app powered by Baker", 5 | "devDependencies": { 6 | "minimist": "^1.2.0", 7 | "remote-redux-devtools": "^0.1.6", 8 | "yeoman-environment": "^1.6.1", 9 | "yeoman-generator": "^0.21.2" 10 | }, 11 | "scripts": { 12 | "setup": "npm install --save --save-exact react-native && node ./cli/baker.js generate:app", 13 | "generate": "node ./cli/baker.js generate" 14 | } 15 | } -------------------------------------------------------------------------------- /baker/generators/test/tests/fixtures/random-file.txt: -------------------------------------------------------------------------------- 1 | just a random file -------------------------------------------------------------------------------- /baker/generators/test/tests/fixtures/reducers.js.template: -------------------------------------------------------------------------------- 1 | import HomeReducer from './containers/Home/reducer'; 2 | import {combineReducers} from 'redux-immutable'; 3 | 4 | // XX: Do not rename this variable if you want reducer generator 5 | // to keep working properly (and you do want that, right?) 6 | const applicationReducers = { 7 | home: HomeReducer 8 | }; 9 | 10 | export default function createReducer() { 11 | return combineReducers(applicationReducers); 12 | } 13 | -------------------------------------------------------------------------------- /baker/generators/test/tests/fixtures/sagas.index.js.template: -------------------------------------------------------------------------------- 1 | import { anotherSaga } from './anotherSaga'; 2 | 3 | // XX: this is used by the code generator 4 | // please do not rename this 5 | const sagas = [ 6 | anotherSaga 7 | ]; 8 | 9 | module.exports = sagas; -------------------------------------------------------------------------------- /baker/generators/test/tests/model.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0, no-unused-expressions:0 */ 2 | 3 | import path from 'path'; 4 | import assert from 'yeoman-assert'; 5 | import helpers from 'yeoman-test'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import fs from 'fs-extra'; 10 | 11 | const expect = chai.expect; 12 | 13 | describe('generator-rn:model', () => { 14 | const appDirectory = 'app'; 15 | const serverDirectory = 'server'; 16 | const modelName = 'Todo'; 17 | 18 | before(done => { 19 | helpers.run(path.join(__dirname, '../../model')) 20 | .withPrompts({ 21 | modelName, 22 | }) 23 | .on('end', done); 24 | }); 25 | 26 | it('creates a model file in server/models directory', () => { 27 | assert.file([ 28 | `${serverDirectory}/models/Todo.js`, 29 | ]); 30 | }); 31 | 32 | it('imports newly created model in server/graphql/schema module', () => { 33 | assert.fileContent(`${serverDirectory}/graphql/schema.js`, 34 | `import ${modelName} from '../models/${modelName}';` 35 | ); 36 | }); 37 | 38 | it('references new model\'s root query in server/graphql/schema module', () => { 39 | assert.fileContent(`${serverDirectory}/graphql/schema.js`, 'todo: Todo.RootQuery'); 40 | }); 41 | 42 | it('references new model\'s mutations in server/graphql/schema module', () => { 43 | assert.fileContent(`${serverDirectory}/graphql/schema.js`, 44 | `${modelName}.Mutations` 45 | ); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /baker/generators/test/tests/navigation.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import assert from 'yeoman-assert'; 3 | import helpers from 'yeoman-test'; 4 | import chai from 'chai'; 5 | import sinon from 'sinon'; 6 | import sinonChai from 'sinon-chai'; 7 | 8 | const expect = chai.expect; 9 | const navigationGeneratorModule = path.join(__dirname, '../../navigation'); 10 | const appDirectory = 'app'; 11 | const componentName = 'Navigation'; 12 | const boilerplateName = 'navigation/Cards'; 13 | 14 | describe('generator-rn:navigation', () => { 15 | let _generator = null; 16 | 17 | before(done => { 18 | helpers.run(navigationGeneratorModule) 19 | .withPrompts({ 20 | componentName, 21 | boilerplateName, 22 | }) 23 | .on('ready', generator => { 24 | _generator = generator; 25 | }) 26 | .on('end', done); 27 | }); 28 | 29 | it('is defined', () => { 30 | expect(_generator).to.be.ok; 31 | }); 32 | 33 | it('creates a container', () => { 34 | assert.file([ 35 | 'index.js', 36 | 'styles.js', 37 | 'reducer.js', 38 | ].map(f => `${appDirectory}/components/${componentName}/${f}`)); 39 | }); 40 | 41 | it('does not include tests', () => { 42 | assert.noFile(`${appDirectory}/components/${componentName}/index.test.js`); 43 | assert.noFile(`${appDirectory}/components/${componentName}/actions.test.js`); 44 | assert.noFile(`${appDirectory}/components/${componentName}/reducer.test.js`); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /baker/generators/test/tests/reducer.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0, no-unused-expressions:0 */ 2 | 3 | import path from 'path'; 4 | import assert from 'yeoman-assert'; 5 | import helpers from 'yeoman-test'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import mkdirp from 'mkdirp'; 10 | import fs from 'fs-extra'; 11 | import mockery from 'mockery'; 12 | 13 | const expect = chai.expect; 14 | const reducerGeneratorModule = path.join(__dirname, '../../reducer'); 15 | 16 | describe('generator-rn:reducer', () => { 17 | const appDirectory = 'app'; 18 | const container = 'Comments'; 19 | const boilerplate = 'Vanila'; 20 | 21 | let runBoilerplateBeforeHookSpy; 22 | let runBoilerplateAfterHookSpy; 23 | 24 | describe('without existing reducers module', () => { 25 | before(done => { 26 | runBoilerplateBeforeHookSpy = sinon.spy(); 27 | runBoilerplateAfterHookSpy = sinon.spy(); 28 | 29 | mockery.enable(); 30 | mockery.warnOnUnregistered(false); 31 | mockery.registerMock('./boilerplates', { 32 | runBoilerplateBeforeHook: runBoilerplateBeforeHookSpy, 33 | runBoilerplateAfterHook: runBoilerplateAfterHookSpy, 34 | }); 35 | 36 | helpers.run(reducerGeneratorModule) 37 | .withOptions({ 38 | container, 39 | boilerplateName: boilerplate, 40 | }).withPrompts({ 41 | appDirectory, 42 | container, 43 | }) 44 | .on('end', done); 45 | }); 46 | 47 | after(() => { 48 | mockery.deregisterAll(); 49 | mockery.disable(); 50 | }); 51 | 52 | it('creates reducer files', () => { 53 | assert.file([ 54 | 'reducer.js', 55 | 'reducer.test.js', 56 | 'actions.js', 57 | 'actions.test.js', 58 | 'constants.js', 59 | ].map(f => `${appDirectory}/components/${container}/${f}`)); 60 | }); 61 | 62 | it('updates root reducers file with new reducer info', () => { 63 | const reducersModulePath = `${appDirectory}/reducers.js`; 64 | assert.file(reducersModulePath); 65 | assert.fileContent(reducersModulePath, 66 | `import comments from './components/${container}/reducer'` 67 | ); 68 | assert.fileContent(reducersModulePath, 69 | 'comments: comments' 70 | ); 71 | }); 72 | 73 | it('default exports newly created reducer ', () => { 74 | const reducerModulePath = `${appDirectory}/components/${container}/reducer.js`; 75 | assert.fileContent(reducerModulePath, 76 | 'export default comments' 77 | ); 78 | }); 79 | 80 | it('exports a selector for the newly created reducer ', () => { 81 | const reducerModulePath = `${appDirectory}/components/${container}/reducer.js`; 82 | assert.fileContent(reducerModulePath, 83 | 'export function selectComments(state) {' 84 | ); 85 | }); 86 | 87 | it('calls boilerplate hooks', () => { 88 | expect(runBoilerplateBeforeHookSpy.calledOnce).to.be.ok; 89 | expect(runBoilerplateBeforeHookSpy.calledWith(boilerplate)).to.be.ok; 90 | expect(runBoilerplateAfterHookSpy.calledOnce).to.be.ok; 91 | expect(runBoilerplateAfterHookSpy.calledWith(boilerplate)).to.be.ok; 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /baker/generators/test/tests/saga.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0, no-unused-expressions:0 */ 2 | 3 | import path from 'path'; 4 | import assert from 'yeoman-assert'; 5 | import helpers from 'yeoman-test'; 6 | import chai from 'chai'; 7 | import sinon from 'sinon'; 8 | import sinonChai from 'sinon-chai'; 9 | import fs from 'fs-extra'; 10 | 11 | const expect = chai.expect; 12 | 13 | describe('generator-rn:saga', () => { 14 | const appDirectory = 'app'; 15 | const sagaName = 'talkToServer'; 16 | 17 | describe('without existing saga index file', () => { 18 | before(done => { 19 | helpers.run(path.join(__dirname, '../../saga')) 20 | .withPrompts({ 21 | sagaName, 22 | }) 23 | .on('end', done); 24 | }); 25 | 26 | it('creates a saga file', () => { 27 | assert.file([ 28 | `${appDirectory}/sagas/${sagaName}.js`, 29 | ]); 30 | }); 31 | 32 | it('eports a saga within saga file', () => { 33 | assert.fileContent(`${appDirectory}/sagas/${sagaName}.js`, 34 | `export function* ${sagaName}()` 35 | ); 36 | }); 37 | 38 | it('creates sagas index file', () => { 39 | assert.file(`${appDirectory}/sagas/index.js`); 40 | }); 41 | 42 | it('imports new saga in sagas/index.js', () => { 43 | assert.fileContent(`${appDirectory}/sagas/index.js`, 44 | `import { ${sagaName} } from './${sagaName}';` 45 | ); 46 | }); 47 | 48 | it('exports new saga in sagas/index.js', () => { 49 | assert.fileContent(`${appDirectory}/sagas/index.js`, 50 | `const sagas = [${sagaName}];` 51 | ); 52 | assert.fileContent(`${appDirectory}/sagas/index.js`, 53 | 'module.exports = sagas;' 54 | ); 55 | }); 56 | }); 57 | 58 | describe('with boilerplate selected', () => { 59 | before(done => { 60 | helpers.run(path.join(__dirname, '../../saga')) 61 | .withPrompts({ 62 | sagaName, 63 | boilerplateName: 'MethodCall', 64 | }) 65 | .on('end', done); 66 | }); 67 | 68 | it('creates a saga file', () => { 69 | assert.file([ 70 | `${appDirectory}/sagas/${sagaName}.js`, 71 | ]); 72 | }); 73 | 74 | it('eports a saga within saga file', () => { 75 | assert.fileContent(`${appDirectory}/sagas/${sagaName}.js`, 76 | `export function* ${sagaName}()` 77 | ); 78 | }); 79 | 80 | it('creates sagas index file', () => { 81 | assert.file(`${appDirectory}/sagas/index.js`); 82 | }); 83 | 84 | it('imports new saga in sagas/index.js', () => { 85 | assert.fileContent(`${appDirectory}/sagas/index.js`, 86 | `import { ${sagaName} } from './${sagaName}';` 87 | ); 88 | }); 89 | 90 | it('exports new saga in sagas/index.js', () => { 91 | assert.fileContent(`${appDirectory}/sagas/index.js`, 92 | `const sagas = [${sagaName}];` 93 | ); 94 | assert.fileContent(`${appDirectory}/sagas/index.js`, 95 | 'module.exports = sagas;' 96 | ); 97 | }); 98 | }); 99 | 100 | describe('with existing saga index file', () => { 101 | before(done => { 102 | helpers.run(path.join(__dirname, '../../saga')) 103 | .inTmpDir(dir => { 104 | fs.copySync( 105 | path.join(__dirname, './fixtures/sagas.index.js.template'), 106 | path.join(dir, `${appDirectory}/sagas/index.js`) 107 | ); 108 | }) 109 | .withArguments(['--force']) 110 | .withPrompts({ 111 | sagaName, 112 | }) 113 | .on('end', done); 114 | }); 115 | 116 | it('keeps original sagas', () => { 117 | assert.fileContent(`${appDirectory}/sagas/index.js`, 118 | 'import { anotherSaga } from \'./anotherSaga\';' 119 | ); 120 | }); 121 | 122 | it('adds new saga module', () => { 123 | assert.file(`${appDirectory}/sagas/${sagaName}.js`); 124 | }); 125 | 126 | it('references new saga module in sagas/index.js', () => { 127 | assert.fileContent(`${appDirectory}/sagas/index.js`, 128 | `import { ${sagaName} } from './${sagaName}'`); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /gradle-bump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Here's a super hacky way to bump versionCode in your gradle build file 4 | // for React Native projects 5 | // Usage: gradle-bump PATH_TO_GRADLE_BUILD 6 | // ---------------------------------------------------------------------- 7 | // Note: this only affects versionCode value and not versionName which is 8 | // something you want to edit manually 9 | 10 | if (process.argv.length < 3) { 11 | console.error('Usage: gradle-bump PATH_TO_GRADLE_BUILD_FILE'); 12 | process.exit(1); 13 | } 14 | 15 | var fs = require('fs'); 16 | var gradleFileLocation = process.argv[2]; 17 | var versionCodeRx = /(versionCode) ([0-9]+)/ig; 18 | var gradleConfigContent = fs.readFileSync(gradleFileLocation).toString(); 19 | var currentVersionCode = parseInt(versionCodeRx.exec(gradleConfigContent)[2], 10); 20 | 21 | fs.writeFileSync(gradleFileLocation, 22 | gradleConfigContent.replace(versionCodeRx, '$1 ' + (currentVersionCode + 1)) 23 | ); 24 | 25 | console.log('Bumped versionCode: ', currentVersionCode, '->', currentVersionCode + 1); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todos", 3 | "version": "1.0.0", 4 | "description": "React Native Todo list", 5 | "author": "", 6 | "license": "ISC", 7 | "scripts": { 8 | "generate": "node ./cli/baker.js generate", 9 | "lint": "./node_modules/.bin/eslint app baker server", 10 | "setup": "node ./cli/baker.js generate:app" 11 | }, 12 | "engines": { 13 | "node": ">=4.3" 14 | }, 15 | "devDependencies": { 16 | "babel-cli": "^6.10.1", 17 | "babel-plugin-transform-object-rest-spread": "^6.8.0", 18 | "babel-polyfill": "^6.9.1", 19 | "babel-preset-es2015": "^6.9.0", 20 | "babel-register": "^6.9.0", 21 | "chai": "^3.5.0", 22 | "change-case": "^2.3.1", 23 | "escodegen": "^1.8.0", 24 | "eslint": "^2.9.0", 25 | "eslint-config-airbnb": "^9.0.1", 26 | "eslint-plugin-import": "^1.10.2", 27 | "eslint-plugin-jsx-a11y": "^1.5.5", 28 | "eslint-plugin-react": "^5.2.2", 29 | "esprima": "^2.7.2", 30 | "fs-extra": "^0.28.0", 31 | "handlebars": "^4.0.5", 32 | "lodash": "^4.11.1", 33 | "minimist": "^1.2.0", 34 | "mkdirp": "^0.5.1", 35 | "mocha": "^2.5.3", 36 | "mockery": "^1.7.0", 37 | "shelljs": "^0.6.0", 38 | "sinon": "^1.17.4", 39 | "sinon-chai": "^2.8.0", 40 | "yeoman-assert": "^2.2.1", 41 | "yeoman-environment": "^1.6.1", 42 | "yeoman-generator": "^0.22.0", 43 | "yeoman-test": "^1.4.0", 44 | "yosay": "^1.0.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/Procfile: -------------------------------------------------------------------------------- 1 | web: npm start -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todos-server", 3 | "version": "1.0.0", 4 | "description": "React Native app powered by Baker", 5 | "author": "", 6 | "license": "ISC", 7 | "scripts": { 8 | "deploy": "./node_modules/babel-cli/bin/babel-node.js ./scripts/server-deploy.js --presets es2015", 9 | "start": "node ./build/index.js", 10 | "heroku-postbuild": "./node_modules/.bin/babel src --presets es2015 -d build", 11 | "mongo": "node ./node_modules/mongodb-runner/bin/mongodb-runner start --name=dev --purge false", 12 | "server": "npm run mongo && NODE_ENV=development ./scripts/server.js", 13 | "server-debug": "npm run mongo && NODE_ENV=development ./scripts/server.js --debug", 14 | "server-watch": "npm run mongo && NODE_ENV=development ./scripts/server.js --watch" 15 | }, 16 | "engines": { 17 | "node": ">=4.3" 18 | }, 19 | "dependencies": { 20 | "babel-cli": "^6.14.0", 21 | "babel-preset-es2015": "^6.14.0", 22 | "body-parser": "^1.15.2", 23 | "express": "^4.13.4", 24 | "express-delay": "^0.1.0", 25 | "graphql": "^0.6.2", 26 | "graphql-server-express": "^0.4.3", 27 | "graphql-tools": "^0.8.3", 28 | "immutable": "^3.8.1", 29 | "parse": "1.8.5", 30 | "parse-dashboard": "^1.0.18", 31 | "parse-graphql-client": "^0.2.0", 32 | "parse-graphql-server": "git://github.com/bakery/parse-graphql-server.git#feature/apollo", 33 | "parse-server": "^2.2.22" 34 | }, 35 | "devDependencies": { 36 | "mongodb-runner": "^3.4.0", 37 | "forever-monitor": "^1.7.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/public/images/todo-mvc-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bakery/todomvc-react-native/9862e18b91cc56cfe684220376842fe63ad8d463/server/public/images/todo-mvc-icon.png -------------------------------------------------------------------------------- /server/scripts/server-deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'path'; 4 | import { execSync as run } from 'child_process'; 5 | import serverSettings from '../../settings/production/server'; 6 | import baseSettings from '../../settings/production/base'; 7 | 8 | const settings = Object.assign(baseSettings, serverSettings); 9 | 10 | console.log('deploying jazz with', settings); 11 | 12 | run(`heroku config:set APPLICATION_SETTINGS='${JSON.stringify(settings)}'`); 13 | run('git subtree push --prefix server heroku master', { 14 | cwd: path.resolve('..'), 15 | }); 16 | -------------------------------------------------------------------------------- /server/scripts/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint no-var:"off", no-console:"off", vars-on-top:"off", prefer-arrow-callback:"off" */ 4 | 5 | var run = require('child_process').execSync; 6 | var argv = require('minimist')(process.argv.slice(2)); 7 | var forever = require('forever-monitor'); 8 | 9 | var options = { 10 | max: 3, 11 | command: './node_modules/babel-cli/bin/babel-node.js', 12 | args: ['--presets', 'es2015', '--plugins', 'transform-object-rest-spread'], 13 | }; 14 | 15 | if (argv.watch) { 16 | console.log('watching'); 17 | options.watch = true; 18 | options.watchDirectory = './src'; 19 | } 20 | 21 | if (argv.debug) { 22 | console.log('debugging'); 23 | options.args.push('--debug'); 24 | } 25 | 26 | var monitor = new (forever.Monitor)('./src', options); 27 | 28 | monitor.on('start', function onMonitorStarted() { 29 | if (argv['test-run']) { 30 | console.log('testing server...'); 31 | setTimeout(function onAfterServerStared() { 32 | run('curl localhost:8000'); 33 | monitor.stop(); 34 | }, 30000); 35 | } 36 | }); 37 | monitor.on('stderr', function onServerScriptError(error) { 38 | console.error(error.toString()); 39 | }); 40 | 41 | monitor.start(); 42 | -------------------------------------------------------------------------------- /server/src/api/todo/model.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/node'; 2 | 3 | const Todo = Parse.Object.extend('Todo'); 4 | 5 | export default Todo; 6 | -------------------------------------------------------------------------------- /server/src/api/todo/resolver.js: -------------------------------------------------------------------------------- 1 | import Parse from 'parse/node'; 2 | import Todo from './model'; 3 | 4 | export default { 5 | Todo: { 6 | text: (root) => root.get('text'), 7 | isComplete: (root) => root.get('isComplete'), 8 | createdAt: (root) => root.get('createdAt').getTime(), 9 | }, 10 | Query: { 11 | todos(root, { isComplete }, { Query, user }) { 12 | const query = new Query(Todo); 13 | 14 | if (typeof isComplete !== 'undefined') { 15 | query.equalTo('isComplete', isComplete); 16 | } 17 | 18 | if (user) { 19 | query.equalTo('user', user); 20 | } 21 | 22 | return query.find(); 23 | }, 24 | }, 25 | Mutation: { 26 | addTodo(_, { text }, { Query, user }) { 27 | const newTodo = new Query(Todo).create({ isComplete: false, text, user }); 28 | if (user) { 29 | newTodo.setACL(new Parse.ACL(user)); 30 | } 31 | return newTodo.save().then(td => td); 32 | }, 33 | 34 | deleteTodo(_, { id }, { Query }) { 35 | return new Query(Todo).get(id).then((todo) => { 36 | if (todo) { 37 | todo.destroy(); 38 | } 39 | return todo; 40 | }); 41 | }, 42 | 43 | toggleTodoCompletion(_, { id }, { Query }) { 44 | return new Query(Todo).get(id).then((todo) => { 45 | return todo.save({ isComplete: !todo.get('isComplete') }).then(td => td); 46 | }); 47 | }, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /server/src/api/todo/schema.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | type Todo { 3 | id: ID! 4 | text: String! 5 | isComplete: Boolean!, 6 | createdAt: Float! 7 | } 8 | 9 | # the schema allows the following query: 10 | type Query { 11 | todos( 12 | isComplete: Boolean 13 | ): [Todo] 14 | } 15 | 16 | # this schema allows the following mutation: 17 | type Mutation { 18 | addTodo ( 19 | text: String! 20 | ): Todo 21 | 22 | deleteTodo ( 23 | id: ID! 24 | ): Todo 25 | 26 | toggleTodoCompletion ( 27 | id: ID! 28 | ): Todo 29 | } 30 | 31 | # we need to tell the server which types represent the root query 32 | # and root mutation types. We call them RootQuery and RootMutation by convention. 33 | schema { 34 | query: Query 35 | mutation: Mutation 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /server/src/graphql/index.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import parseGraphQLHTTP from 'parse-graphql-server'; 3 | import { graphiqlExpress } from 'graphql-server-express'; 4 | import schema from './schema'; 5 | 6 | export default { 7 | setup(app) { 8 | const IS_DEVELOPMENT = process.env.NODE_ENV === 'development'; 9 | 10 | app.use(bodyParser.urlencoded({ extended: true })); 11 | app.use(bodyParser.json()); 12 | app.use('/graphql', parseGraphQLHTTP({ schema })); 13 | 14 | if (IS_DEVELOPMENT) { 15 | app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); 16 | } 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /server/src/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import { makeExecutableSchema } from 'graphql-tools'; 2 | import TodoSchema from '../api/todo/schema'; 3 | import TodoResolvers from '../api/todo/resolver'; 4 | 5 | const schema = makeExecutableSchema({ 6 | typeDefs: [TodoSchema], 7 | resolvers: TodoResolvers, 8 | }); 9 | 10 | export default schema; 11 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | import packageJSON from '../package'; 2 | import express from 'express'; 3 | import delay from 'express-delay'; 4 | import graphql from './graphql'; 5 | import parseServer from './parse-server'; 6 | 7 | function loadSettings() { 8 | // try loading local settings inside shared settings directory 9 | try { 10 | return Object.assign({}, 11 | // eslint-disable-next-line global-require 12 | require('../../settings/development/base'), 13 | // eslint-disable-next-line global-require 14 | require('../../settings/development/server') 15 | ); 16 | } catch (e) { 17 | return JSON.parse(process.env.APPLICATION_SETTINGS); 18 | } 19 | } 20 | 21 | const settings = loadSettings(); 22 | const app = express(); 23 | const serverPort = process.env.PORT || settings.serverPort; 24 | 25 | // XX: delay all the responses 26 | app.use(delay(0)); 27 | 28 | parseServer.setup(app, packageJSON.name, settings); 29 | graphql.setup(app); 30 | 31 | app.listen(serverPort); 32 | -------------------------------------------------------------------------------- /server/src/parse-server/index.js: -------------------------------------------------------------------------------- 1 | import { ParseServer } from 'parse-server'; 2 | import Parse from 'parse/node'; 3 | import ParseDashboard from 'parse-dashboard'; 4 | 5 | export default { 6 | setup (app, appName, settings) { 7 | Parse.initialize(settings.parseServerApplicationId, 'js-key', settings.parseServerMasterKey); 8 | Parse.serverURL = settings.parseServerURL; 9 | 10 | const api = new ParseServer({ 11 | appId: settings.parseServerApplicationId, 12 | masterKey: settings.parseServerMasterKey, 13 | serverURL: settings.parseServerURL, 14 | databaseURI: settings.parseServerDatabaseURI 15 | }); 16 | 17 | app.use('/parse', api); 18 | 19 | app.use( 20 | '/dashboard', 21 | ParseDashboard({ 22 | apps: [{ 23 | serverURL: settings.parseServerURL, 24 | appId: settings.parseServerApplicationId, 25 | masterKey: settings.parseServerMasterKey, 26 | appName, 27 | iconName: 'todo-mvc-icon.png' 28 | }], 29 | iconsFolder: 'server/public/images', 30 | // XX: fix this 31 | users: [ 32 | { 33 | "user": "admin", 34 | "pass": "admin" 35 | } 36 | ] 37 | }, true) 38 | ); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /settings: -------------------------------------------------------------------------------- 1 | ./app/settings/ --------------------------------------------------------------------------------