Repository: infinitered/ignite-andross Branch: master Commit: c0836e755a6b Files: 127 Total size: 157.8 KB Directory structure: gitextract_lzwitwv2/ ├── .circleci/ │ └── config.yml ├── .gitignore ├── LICENSE ├── boilerplate/ │ ├── .babelrc │ ├── .editorconfig │ ├── App/ │ │ ├── Components/ │ │ │ ├── AlertMessage.js │ │ │ ├── AlertMessage.story.js │ │ │ ├── DrawerButton.js │ │ │ ├── DrawerButton.story.js │ │ │ ├── FullButton.js │ │ │ ├── FullButton.story.js │ │ │ ├── README.md │ │ │ ├── RoundedButton.js │ │ │ ├── RoundedButton.story.js │ │ │ ├── Stories.js │ │ │ └── Styles/ │ │ │ ├── AlertMessageStyles.js │ │ │ ├── DrawerButtonStyles.js │ │ │ ├── FullButtonStyles.js │ │ │ ├── README.md │ │ │ └── RoundedButtonStyles.js │ │ ├── Config/ │ │ │ ├── AppConfig.js │ │ │ ├── DebugConfig.js │ │ │ ├── README.md │ │ │ ├── ReactotronConfig.js │ │ │ └── index.js │ │ ├── Containers/ │ │ │ ├── App.js │ │ │ ├── LaunchScreen.js │ │ │ ├── README.md │ │ │ ├── RootContainer.js │ │ │ └── Styles/ │ │ │ ├── LaunchScreenStyles.js │ │ │ ├── README.md │ │ │ └── RootContainerStyles.js │ │ ├── Fixtures/ │ │ │ ├── README.md │ │ │ ├── gantman.json │ │ │ ├── rateLimit.json │ │ │ ├── root.json │ │ │ └── skellock.json │ │ ├── Images/ │ │ │ └── README.md │ │ ├── Lib/ │ │ │ └── README.md │ │ ├── Navigation/ │ │ │ ├── AppNavigation.js │ │ │ ├── ReduxNavigation.js │ │ │ └── Styles/ │ │ │ └── NavigationStyles.js │ │ ├── Redux/ │ │ │ ├── CreateStore.js │ │ │ ├── GithubRedux.js │ │ │ ├── NavigationRedux.js │ │ │ ├── ScreenTrackingMiddleware.js │ │ │ ├── SearchRedux.js │ │ │ ├── StartupRedux.js │ │ │ └── index.js │ │ ├── Sagas/ │ │ │ ├── GithubSagas.js │ │ │ ├── StartupSagas.js │ │ │ └── index.js │ │ ├── Services/ │ │ │ ├── Api.js │ │ │ ├── ExamplesRegistry.js │ │ │ └── FixtureApi.js │ │ ├── Themes/ │ │ │ ├── ApplicationStyles.js │ │ │ ├── Colors.js │ │ │ ├── Fonts.js │ │ │ ├── Images.js │ │ │ ├── Metrics.js │ │ │ ├── README.md │ │ │ └── index.js │ │ └── Transforms/ │ │ ├── ConvertFromKelvin.js │ │ └── README.md │ ├── README.md │ ├── Tests/ │ │ ├── Components/ │ │ │ ├── AlertMessageTest.js │ │ │ ├── DrawerButtonTest.js │ │ │ ├── FullButtonTest.js │ │ │ └── RoundedButtonTest.js │ │ ├── Redux/ │ │ │ └── GithubReduxTest.js │ │ ├── Sagas/ │ │ │ ├── GithubSagaTest.js │ │ │ └── StartupSagaTest.js │ │ ├── Services/ │ │ │ └── FixtureAPITest.js │ │ ├── Setup.js.ejs │ │ └── StoriesTest.js │ ├── ignite.json.ejs │ ├── index.js.ejs │ ├── package.json.ejs │ └── storybook/ │ ├── addons.js │ ├── index.js │ └── storybook.ejs ├── boilerplate.js ├── commands/ │ └── generate/ │ ├── component.js │ ├── container.js │ ├── generate.js │ ├── list.js │ ├── redux.js │ ├── saga.js │ └── screen.js ├── ignite.json ├── lib/ │ ├── patterns.js │ └── react-native-version.js ├── options.js ├── package.json ├── plugin.js ├── readme.md ├── screenExamples.js ├── templates/ │ ├── component-style.ejs │ ├── component-test.ejs │ ├── component.ejs │ ├── container-style.ejs │ ├── container.ejs │ ├── examples/ │ │ ├── GridExample.js.ejs │ │ ├── RowExample.js.ejs │ │ ├── SectionExample.js.ejs │ │ └── Styles/ │ │ ├── GridExampleStyle.js.ejs │ │ ├── RowExampleStyle.js.ejs │ │ └── SectionExampleStyle.js.ejs │ ├── flatlist-grid-style.ejs │ ├── flatlist-grid.ejs │ ├── flatlist-sections.ejs │ ├── flatlist.ejs │ ├── listview-grid-style.ejs │ ├── listview-sections.ejs │ ├── listview-style.ejs │ ├── listview.ejs │ ├── redux-test-ava.ejs │ ├── redux-test-jest.ejs │ ├── redux.ejs │ ├── saga-test-ava.ejs │ ├── saga-test-jest.ejs │ ├── saga.ejs │ ├── screen-style.ejs │ └── screen.ejs └── test/ ├── generators-integration.test.js ├── interface.test.js └── react-native-version.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .circleci/config.yml ================================================ # Javascript Node CircleCI 2.0 configuration file # # Check https://circleci.com/docs/2.0/language-javascript/ for more details # defaults: &defaults docker: # Choose the version of Node you want here - image: circleci/node:12.16 working_directory: ~/repo version: 2 jobs: setup: <<: *defaults steps: - checkout - restore_cache: name: Restore node modules keys: - v1-dependencies-{{ checksum "package.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: name: Install dependencies command: | yarn install - save_cache: name: Save node modules paths: - node_modules key: v1-dependencies-{{ checksum "package.json" }} tests: <<: *defaults steps: - checkout - restore_cache: name: Restore node modules keys: - v1-dependencies-{{ checksum "package.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- - run: name: Run tests command: yarn ci:test # this command will be added to/found in your package.json scripts publish: <<: *defaults steps: - checkout - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - restore_cache: name: Restore node modules keys: - v1-dependencies-{{ checksum "package.json" }} # fallback to using the latest cache if no exact match is found - v1-dependencies- # Run semantic-release after all the above is set. - run: name: Publish to NPM command: yarn ci:publish # this will be added to your package.json scripts workflows: version: 2 test_and_release: jobs: - setup - tests: requires: - setup - publish: requires: - tests filters: branches: only: master ================================================ FILE: .gitignore ================================================ .DS_Store npm-debug.log npm-debug.log* coverage .nyc_output lerna-debug.log node_modules .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json testgrounds IntegrationTest integration_test .idea yarn-error.log .vscode package-lock.json ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Infinite Red, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: boilerplate/.babelrc ================================================ { "presets": ["module:metro-react-native-babel-preset"], "env": { "production": { "plugins": ["ignite-ignore-reactotron"] } } } ================================================ FILE: boilerplate/.editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true [*.gradle] indent_size = 4 ================================================ FILE: boilerplate/App/Components/AlertMessage.js ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types' import { View, Text } from 'react-native' import styles from './Styles/AlertMessageStyles' export default class AlertMessage extends Component { static defaultProps = { show: true } static propTypes = { title: PropTypes.string, icon: PropTypes.string, style: PropTypes.object, show: PropTypes.bool } render () { let messageComponent = null if (this.props.show) { const { title } = this.props return ( {title && title.toUpperCase()} ) } return messageComponent } } ================================================ FILE: boilerplate/App/Components/AlertMessage.story.js ================================================ import React from 'react' import { storiesOf } from '@storybook/react-native' import AlertMessage from './AlertMessage' storiesOf('AlertMessage') .add('Default', () => ( )) .add('Hidden', () => ( )) .add('Custom Style', () => ( )) ================================================ FILE: boilerplate/App/Components/DrawerButton.js ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types' import { Text, TouchableOpacity } from 'react-native' import styles from './Styles/DrawerButtonStyles' import ExamplesRegistry from '../Services/ExamplesRegistry' // Note that this file (App/Components/DrawerButton) needs to be // imported in your app somewhere, otherwise your component won't be // compiled and added to the examples dev screen. // Ignore in coverage report /* istanbul ignore next */ ExamplesRegistry.addComponentExample('Drawer Button', () => window.alert('Your drawers are showing')} /> ) class DrawerButton extends Component { static propTypes = { text: PropTypes.string, onPress: PropTypes.func } render () { return ( {this.props.text} ) } } export default DrawerButton ================================================ FILE: boilerplate/App/Components/DrawerButton.story.js ================================================ import React from 'react' import { View } from 'react-native' import { storiesOf } from '@storybook/react-native' import DrawerButton from './DrawerButton' storiesOf('DrawerButton') .add('Default', () => ( { }} /> )) ================================================ FILE: boilerplate/App/Components/FullButton.js ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types' import { TouchableOpacity, Text } from 'react-native' import styles from './Styles/FullButtonStyles' import ExamplesRegistry from '../Services/ExamplesRegistry' // Note that this file (App/Components/FullButton) needs to be // imported in your app somewhere, otherwise your component won't be // compiled and added to the examples dev screen. // Ignore in coverage report /* istanbul ignore next */ ExamplesRegistry.addComponentExample('Full Button', () => window.alert('Full Button Pressed!')} /> ) export default class FullButton extends Component { static propTypes = { text: PropTypes.string, onPress: PropTypes.func, styles: PropTypes.object } render () { return ( {this.props.text && this.props.text.toUpperCase()} ) } } ================================================ FILE: boilerplate/App/Components/FullButton.story.js ================================================ import React from 'react' import { storiesOf } from '@storybook/react-native' import FullButton from './FullButton' storiesOf('FullButton') .add('Default', () => ( )) .add('Custom Style', () => ( )) ================================================ FILE: boilerplate/App/Components/README.md ================================================ ### Components Folder All components are stored and organized here ================================================ FILE: boilerplate/App/Components/RoundedButton.js ================================================ import React, { Component } from 'react' import PropTypes from 'prop-types' import { TouchableOpacity, Text } from 'react-native' import styles from './Styles/RoundedButtonStyles' import ExamplesRegistry from '../Services/ExamplesRegistry' // Note that this file (App/Components/RoundedButton) needs to be // imported in your app somewhere, otherwise your component won't be // compiled and added to the examples dev screen. // Ignore in coverage report /* istanbul ignore next */ ExamplesRegistry.addComponentExample('Rounded Button', () => window.alert('Rounded Button Pressed!')} /> ) export default class RoundedButton extends Component { static propTypes = { onPress: PropTypes.func, text: PropTypes.string, children: PropTypes.string, navigator: PropTypes.object } getText () { const buttonText = this.props.text || this.props.children || '' return buttonText.toUpperCase() } render () { return ( {this.getText()} ) } } ================================================ FILE: boilerplate/App/Components/RoundedButton.story.js ================================================ import React from 'react' import { storiesOf } from '@storybook/react-native' import RoundedButton from './RoundedButton' storiesOf('RoundedButton') .add('Default', () => ( )) .add('Text as children', () => ( Hello from the children! )) ================================================ FILE: boilerplate/App/Components/Stories.js ================================================ import './AlertMessage.story' import './DrawerButton.story' import './FullButton.story' import './RoundedButton.story' ================================================ FILE: boilerplate/App/Components/Styles/AlertMessageStyles.js ================================================ import { StyleSheet } from 'react-native' import { Colors, Metrics, Fonts } from '../../Themes/' export default StyleSheet.create({ container: { justifyContent: 'center', marginVertical: Metrics.section }, contentContainer: { alignSelf: 'center', alignItems: 'center' }, message: { marginTop: Metrics.baseMargin, marginHorizontal: Metrics.baseMargin, textAlign: 'center', fontFamily: Fonts.type.base, fontSize: Fonts.size.regular, fontWeight: 'bold', color: Colors.steel }, icon: { color: Colors.steel } }) ================================================ FILE: boilerplate/App/Components/Styles/DrawerButtonStyles.js ================================================ import { Metrics, Colors, Fonts } from '../../Themes' export default { text: { ...Fonts.style.h5, color: Colors.snow, marginVertical: Metrics.baseMargin } } ================================================ FILE: boilerplate/App/Components/Styles/FullButtonStyles.js ================================================ import { StyleSheet } from 'react-native' import { Fonts, Colors } from '../../Themes/' export default StyleSheet.create({ button: { marginVertical: 5, borderTopColor: Colors.fire, borderBottomColor: Colors.bloodOrange, borderTopWidth: 1, borderBottomWidth: 1, backgroundColor: Colors.ember }, buttonText: { margin: 18, textAlign: 'center', color: Colors.snow, fontSize: Fonts.size.medium, fontFamily: Fonts.type.bold } }) ================================================ FILE: boilerplate/App/Components/Styles/README.md ================================================ ### Styles Folder Component styles are separated from functionality. ================================================ FILE: boilerplate/App/Components/Styles/RoundedButtonStyles.js ================================================ import { StyleSheet } from 'react-native' import { Fonts, Colors, Metrics } from '../../Themes/' export default StyleSheet.create({ button: { height: 45, borderRadius: 5, marginHorizontal: Metrics.section, marginVertical: Metrics.baseMargin, backgroundColor: Colors.fire, justifyContent: 'center' }, buttonText: { color: Colors.snow, textAlign: 'center', fontWeight: 'bold', fontSize: Fonts.size.medium, marginVertical: Metrics.baseMargin } }) ================================================ FILE: boilerplate/App/Config/AppConfig.js ================================================ // Simple React Native specific changes export default { // font scaling override - RN default is on allowTextFontScaling: true } ================================================ FILE: boilerplate/App/Config/DebugConfig.js ================================================ export default { useFixtures: false, ezLogin: false, yellowBox: __DEV__, reduxLogging: __DEV__, includeExamples: __DEV__, useReactotron: __DEV__ } ================================================ FILE: boilerplate/App/Config/README.md ================================================ ### Config Folder All application specific configuration falls in this folder. `AppConfig.js` - production values. `DebugConfig.js` - development-wide globals. `ReactotronConfig.js` - Reactotron client settings. `ReduxPersist.js` - rehydrate Redux state. ================================================ FILE: boilerplate/App/Config/ReactotronConfig.js ================================================ import Config from '../Config/DebugConfig' import Immutable from 'seamless-immutable' import Reactotron from 'reactotron-react-native' import { reactotronRedux as reduxPlugin } from 'reactotron-redux' import sagaPlugin from 'reactotron-redux-saga' const reactotron = Reactotron .configure({ name: 'Ignite App' }) .useReactNative() .use(reduxPlugin({ onRestore: Immutable })) .use(sagaPlugin()) if (Config.useReactotron) { // https://github.com/infinitered/reactotron for more options! reactotron.connect() // Let's clear Reactotron on every time we load the app reactotron.clear() // Totally hacky, but this allows you to not both importing reactotron-react-native // on every file. This is just DEV mode, so no big deal. } export default reactotron console.tron = reactotron ================================================ FILE: boilerplate/App/Config/index.js ================================================ import DebugConfig from './DebugConfig' import AppConfig from './AppConfig' // eslint-disable-line no-unused-vars if (__DEV__) { // If ReactNative's yellow box warnings are too much, it is possible to turn // it off, but the healthier approach is to fix the warnings. =) console.disableYellowBox = !DebugConfig.yellowBox } ================================================ FILE: boilerplate/App/Containers/App.js ================================================ import '../Config' import DebugConfig from '../Config/DebugConfig' import React, { Component } from 'react' import { Provider } from 'react-redux' import RootContainer from './RootContainer' import createStore from '../Redux' // create our store const store = createStore() /** * Provides an entry point into our application. Both index.ios.js and index.android.js * call this component first. * * We create our Redux store here, put it into a provider and then bring in our * RootContainer. * * We separate like this to play nice with React Native's hot reloading. */ class App extends Component { render () { return ( ) } } // allow reactotron overlay for fast design in dev mode export default DebugConfig.useReactotron ? console.tron.overlay(App) : App ================================================ FILE: boilerplate/App/Containers/LaunchScreen.js ================================================ import React, { Component } from 'react' import { ScrollView, Text, Image, View } from 'react-native' import { Images } from '../Themes' // Styles import styles from './Styles/LaunchScreenStyles' export default class LaunchScreen extends Component { render () { return ( This probably isn't what your app is going to look like. Unless your designer handed you this screen and, in that case, congrats! You're ready to ship. For everyone else, this is where you'll see a live preview of your fully functioning app using Ignite. ) } } ================================================ FILE: boilerplate/App/Containers/README.md ================================================ ### Containers Folder A container is what they call a "Smart Component" in Redux. It is a component which knows about Redux. They are usually used as "Screens". Also located in here are 2 special containers: `App.js` and `RootContainer.js`. `App.js` is first component loaded after `index.ios.js` or `index.android.js`. The purpose of this file is to setup Redux or any other non-visual "global" modules. Having Redux setup here helps with the hot-reloading process in React Native during development as it won't try to reload your sagas and reducers should your colors change (for example). `RootContainer.js` is the first visual component in the app. It is the ancestor of all other screens and components. You'll probably find you'll have great mileage in Ignite apps without even touching these 2 files. They, of course, belong to you, so when you're ready to add something non-visual like Firebase or something visual like an overlay, you have spots to place these additions. ================================================ FILE: boilerplate/App/Containers/RootContainer.js ================================================ import React, { Component } from 'react' import { View, StatusBar } from 'react-native' import ReduxNavigation from '../Navigation/ReduxNavigation' import { connect } from 'react-redux' import StartupActions from '../Redux/StartupRedux' // Styles import styles from './Styles/RootContainerStyles' class RootContainer extends Component { componentDidMount () { this.props.startup() } render () { return ( ) } } // wraps dispatch to create nicer functions to call within our component const mapDispatchToProps = (dispatch) => ({ startup: () => dispatch(StartupActions.startup()) }) export default connect(null, mapDispatchToProps)(RootContainer) ================================================ FILE: boilerplate/App/Containers/Styles/LaunchScreenStyles.js ================================================ import { StyleSheet } from 'react-native' import { Metrics, ApplicationStyles } from '../../Themes/' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { paddingBottom: Metrics.baseMargin }, logo: { marginTop: Metrics.doubleSection, height: Metrics.images.logo, width: Metrics.images.logo, resizeMode: 'contain' }, centered: { alignItems: 'center' } }) ================================================ FILE: boilerplate/App/Containers/Styles/README.md ================================================ ### Styles Folder Container styles are separated from functionality. ================================================ FILE: boilerplate/App/Containers/Styles/RootContainerStyles.js ================================================ import {StyleSheet} from 'react-native' import {Fonts, Metrics, Colors} from '../../Themes/' export default StyleSheet.create({ applicationView: { flex: 1 }, container: { flex: 1, justifyContent: 'center', backgroundColor: Colors.background }, welcome: { fontSize: 20, textAlign: 'center', fontFamily: Fonts.type.base, margin: Metrics.baseMargin }, myImage: { width: 200, height: 200, alignSelf: 'center' } }) ================================================ FILE: boilerplate/App/Fixtures/README.md ================================================ ### Fixtures folder All key API responses are housed here. These API responses can be used for several reasons. _E.G._: * To bypass logins when building any screen of the application * To quickly test API parsing in unit tests * To separate Network from Data concerns while coding ================================================ FILE: boilerplate/App/Fixtures/gantman.json ================================================ { "total_count": 7, "incomplete_results": false, "items": [ { "login": "GantMan", "id": 997157, "avatar_url": "https://avatars.githubusercontent.com/u/997157?v=3", "gravatar_id": "", "url": "https://api.github.com/users/GantMan", "html_url": "https://github.com/GantMan", "followers_url": "https://api.github.com/users/GantMan/followers", "following_url": "https://api.github.com/users/GantMan/following{/other_user}", "gists_url": "https://api.github.com/users/GantMan/gists{/gist_id}", "starred_url": "https://api.github.com/users/GantMan/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/GantMan/subscriptions", "organizations_url": "https://api.github.com/users/GantMan/orgs", "repos_url": "https://api.github.com/users/GantMan/repos", "events_url": "https://api.github.com/users/GantMan/events{/privacy}", "received_events_url": "https://api.github.com/users/GantMan/received_events", "type": "User", "site_admin": false, "score": 122.12115 }, { "login": "vlad-G", "id": 13520880, "avatar_url": "https://avatars.githubusercontent.com/u/13520880?v=3", "gravatar_id": "", "url": "https://api.github.com/users/vlad-G", "html_url": "https://github.com/vlad-G", "followers_url": "https://api.github.com/users/vlad-G/followers", "following_url": "https://api.github.com/users/vlad-G/following{/other_user}", "gists_url": "https://api.github.com/users/vlad-G/gists{/gist_id}", "starred_url": "https://api.github.com/users/vlad-G/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/vlad-G/subscriptions", "organizations_url": "https://api.github.com/users/vlad-G/orgs", "repos_url": "https://api.github.com/users/vlad-G/repos", "events_url": "https://api.github.com/users/vlad-G/events{/privacy}", "received_events_url": "https://api.github.com/users/vlad-G/received_events", "type": "User", "site_admin": false, "score": 12.69848 }, { "login": "gantmani", "id": 3034094, "avatar_url": "https://avatars.githubusercontent.com/u/3034094?v=3", "gravatar_id": "", "url": "https://api.github.com/users/gantmani", "html_url": "https://github.com/gantmani", "followers_url": "https://api.github.com/users/gantmani/followers", "following_url": "https://api.github.com/users/gantmani/following{/other_user}", "gists_url": "https://api.github.com/users/gantmani/gists{/gist_id}", "starred_url": "https://api.github.com/users/gantmani/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gantmani/subscriptions", "organizations_url": "https://api.github.com/users/gantmani/orgs", "repos_url": "https://api.github.com/users/gantmani/repos", "events_url": "https://api.github.com/users/gantmani/events{/privacy}", "received_events_url": "https://api.github.com/users/gantmani/received_events", "type": "User", "site_admin": false, "score": 11.641713 }, { "login": "sgantman", "id": 5911526, "avatar_url": "https://avatars.githubusercontent.com/u/5911526?v=3", "gravatar_id": "", "url": "https://api.github.com/users/sgantman", "html_url": "https://github.com/sgantman", "followers_url": "https://api.github.com/users/sgantman/followers", "following_url": "https://api.github.com/users/sgantman/following{/other_user}", "gists_url": "https://api.github.com/users/sgantman/gists{/gist_id}", "starred_url": "https://api.github.com/users/sgantman/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/sgantman/subscriptions", "organizations_url": "https://api.github.com/users/sgantman/orgs", "repos_url": "https://api.github.com/users/sgantman/repos", "events_url": "https://api.github.com/users/sgantman/events{/privacy}", "received_events_url": "https://api.github.com/users/sgantman/received_events", "type": "User", "site_admin": false, "score": 7.926345 }, { "login": "michaelgantman", "id": 16693070, "avatar_url": "https://avatars.githubusercontent.com/u/16693070?v=3", "gravatar_id": "", "url": "https://api.github.com/users/michaelgantman", "html_url": "https://github.com/michaelgantman", "followers_url": "https://api.github.com/users/michaelgantman/followers", "following_url": "https://api.github.com/users/michaelgantman/following{/other_user}", "gists_url": "https://api.github.com/users/michaelgantman/gists{/gist_id}", "starred_url": "https://api.github.com/users/michaelgantman/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/michaelgantman/subscriptions", "organizations_url": "https://api.github.com/users/michaelgantman/orgs", "repos_url": "https://api.github.com/users/michaelgantman/repos", "events_url": "https://api.github.com/users/michaelgantman/events{/privacy}", "received_events_url": "https://api.github.com/users/michaelgantman/received_events", "type": "User", "site_admin": false, "score": 7.926345 }, { "login": "gantmanis", "id": 19141249, "avatar_url": "https://avatars.githubusercontent.com/u/19141249?v=3", "gravatar_id": "", "url": "https://api.github.com/users/gantmanis", "html_url": "https://github.com/gantmanis", "followers_url": "https://api.github.com/users/gantmanis/followers", "following_url": "https://api.github.com/users/gantmanis/following{/other_user}", "gists_url": "https://api.github.com/users/gantmanis/gists{/gist_id}", "starred_url": "https://api.github.com/users/gantmanis/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/gantmanis/subscriptions", "organizations_url": "https://api.github.com/users/gantmanis/orgs", "repos_url": "https://api.github.com/users/gantmanis/repos", "events_url": "https://api.github.com/users/gantmanis/events{/privacy}", "received_events_url": "https://api.github.com/users/gantmanis/received_events", "type": "User", "site_admin": false, "score": 7.8813524 }, { "login": "Gantman2014", "id": 7669410, "avatar_url": "https://avatars.githubusercontent.com/u/7669410?v=3", "gravatar_id": "", "url": "https://api.github.com/users/Gantman2014", "html_url": "https://github.com/Gantman2014", "followers_url": "https://api.github.com/users/Gantman2014/followers", "following_url": "https://api.github.com/users/Gantman2014/following{/other_user}", "gists_url": "https://api.github.com/users/Gantman2014/gists{/gist_id}", "starred_url": "https://api.github.com/users/Gantman2014/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Gantman2014/subscriptions", "organizations_url": "https://api.github.com/users/Gantman2014/orgs", "repos_url": "https://api.github.com/users/Gantman2014/repos", "events_url": "https://api.github.com/users/Gantman2014/events{/privacy}", "received_events_url": "https://api.github.com/users/Gantman2014/received_events", "type": "User", "site_admin": false, "score": 7.8813524 } ] } ================================================ FILE: boilerplate/App/Fixtures/rateLimit.json ================================================ { "resources": { "core": { "limit": 60, "remaining": 42, "reset": 1488126913 }, "search": { "limit": 10, "remaining": 9, "reset": 1488126003 } }, "rate": { "limit": 60, "remaining": 42, "reset": 1488126913 } } ================================================ FILE: boilerplate/App/Fixtures/root.json ================================================ { "current_user_url": "https://api.github.com/user", "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}", "authorizations_url": "https://api.github.com/authorizations", "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}", "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}", "emails_url": "https://api.github.com/user/emails", "emojis_url": "https://api.github.com/emojis", "events_url": "https://api.github.com/events", "feeds_url": "https://api.github.com/feeds", "followers_url": "https://api.github.com/user/followers", "following_url": "https://api.github.com/user/following{/target}", "gists_url": "https://api.github.com/gists{/gist_id}", "hub_url": "https://api.github.com/hub", "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}", "issues_url": "https://api.github.com/issues", "keys_url": "https://api.github.com/user/keys", "notifications_url": "https://api.github.com/notifications", "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}", "organization_url": "https://api.github.com/orgs/{org}", "public_gists_url": "https://api.github.com/gists/public", "rate_limit_url": "https://api.github.com/rate_limit", "repository_url": "https://api.github.com/repos/{owner}/{repo}", "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}", "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}", "starred_url": "https://api.github.com/user/starred{/owner}{/repo}", "starred_gists_url": "https://api.github.com/gists/starred", "team_url": "https://api.github.com/teams", "user_url": "https://api.github.com/users/{user}", "user_organizations_url": "https://api.github.com/user/orgs", "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}" } ================================================ FILE: boilerplate/App/Fixtures/skellock.json ================================================ { "total_count": 1, "incomplete_results": false, "items": [ { "login": "skellock", "id": 68273, "avatar_url": "https://avatars.githubusercontent.com/u/68273?v=3", "gravatar_id": "", "url": "https://api.github.com/users/skellock", "html_url": "https://github.com/skellock", "followers_url": "https://api.github.com/users/skellock/followers", "following_url": "https://api.github.com/users/skellock/following{/other_user}", "gists_url": "https://api.github.com/users/skellock/gists{/gist_id}", "starred_url": "https://api.github.com/users/skellock/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/skellock/subscriptions", "organizations_url": "https://api.github.com/users/skellock/orgs", "repos_url": "https://api.github.com/users/skellock/repos", "events_url": "https://api.github.com/users/skellock/events{/privacy}", "received_events_url": "https://api.github.com/users/skellock/received_events", "type": "User", "site_admin": false, "score": 107.22611 } ] } ================================================ FILE: boilerplate/App/Images/README.md ================================================ ### Images folder Holds all images for the applications. ================================================ FILE: boilerplate/App/Lib/README.md ================================================ # Lib At first glance, this could appear to be a "miscellaneous" folder, but we recommend that you treat this as proving ground for components that could be reusable outside your project. Maybe you're writing a set of utilities that you could use outside your project, but they're not quite ready or battle tested. This folder would be a great place to put them. They ideally be pure functions have no dependencies on other things in your App folder. ================================================ FILE: boilerplate/App/Navigation/AppNavigation.js ================================================ import { createAppContainer } from 'react-navigation' import { createStackNavigator } from 'react-navigation-stack'; import LaunchScreen from '../Containers/LaunchScreen' import styles from './Styles/NavigationStyles' // Manifest of possible screens const PrimaryNav = createStackNavigator({ LaunchScreen: { screen: LaunchScreen } }, { // Default config for all screens headerMode: 'none', initialRouteName: 'LaunchScreen', navigationOptions: { headerStyle: styles.header } }) export default createAppContainer(PrimaryNav) ================================================ FILE: boilerplate/App/Navigation/ReduxNavigation.js ================================================ import * as React from 'react' import { BackHandler, Platform } from 'react-native' import { createReactNavigationReduxMiddleware, createReduxContainer } from 'react-navigation-redux-helpers' import { connect } from 'react-redux' import AppNavigation from './AppNavigation' export const appNavigatorMiddleware = createReactNavigationReduxMiddleware( (state) => state.nav, 'root' ) const ReduxAppNavigator = createReduxContainer(AppNavigation, 'root') class ReduxNavigation extends React.Component { componentDidMount () { if (Platform.OS === 'ios') return BackHandler.addEventListener('hardwareBackPress', () => { const { dispatch, nav } = this.props // change to whatever is your first screen, otherwise unpredictable results may occur if (nav.routes.length === 1 && (nav.routes[0].routeName === 'LaunchScreen')) { return false } // if (shouldCloseApp(nav)) return false dispatch({ type: 'Navigation/BACK' }) return true }) } componentWillUnmount () { if (Platform.OS === 'ios') return BackHandler.removeEventListener('hardwareBackPress', undefined) } render () { return } } const mapStateToProps = state => ({ nav: state.nav }) export default connect(mapStateToProps)(ReduxNavigation) ================================================ FILE: boilerplate/App/Navigation/Styles/NavigationStyles.js ================================================ import { StyleSheet } from 'react-native' import { Colors } from '../../Themes/' export default StyleSheet.create({ header: { backgroundColor: Colors.backgroundColor } }) ================================================ FILE: boilerplate/App/Redux/CreateStore.js ================================================ import { createStore, applyMiddleware, compose } from 'redux' import Config from '../Config/DebugConfig' import createSagaMiddleware from 'redux-saga' import ScreenTracking from './ScreenTrackingMiddleware' import { appNavigatorMiddleware } from '../Navigation/ReduxNavigation' import Reactotron from '../Config/ReactotronConfig' // creates the store export default (rootReducer, rootSaga) => { /* ------------- Redux Configuration ------------- */ const middleware = [] const enhancers = [] /* ------------- Navigation Middleware ------------ */ middleware.push(appNavigatorMiddleware) /* ------------- Analytics Middleware ------------- */ middleware.push(ScreenTracking) /* ------------- Saga Middleware ------------- */ const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null const sagaMiddleware = createSagaMiddleware({ sagaMonitor }) middleware.push(sagaMiddleware) /* ------------- Assemble Middleware ------------- */ enhancers.push(applyMiddleware(...middleware)) // if Reactotron is enabled (default for __DEV__), we'll create the store through Reactotron const createAppropriateStore = createStore if (Config.useReactotron) { enhancers.push(Reactotron.createEnhancer()) } const store = createAppropriateStore(rootReducer, compose(...enhancers)) // kick off root saga let sagasManager = sagaMiddleware.run(rootSaga) return { store, sagasManager, sagaMiddleware } } ================================================ FILE: boilerplate/App/Redux/GithubRedux.js ================================================ import { createReducer, createActions } from 'reduxsauce' import Immutable from 'seamless-immutable' /* ------------- Types and Action Creators ------------- */ const { Types, Creators } = createActions({ userRequest: ['username'], userSuccess: ['avatar'], userFailure: null }) export const GithubTypes = Types export default Creators /* ------------- Initial State ------------- */ export const INITIAL_STATE = Immutable({ avatar: null, fetching: null, error: null, username: null }) /* ------------- Selectors ------------- */ export const GithubSelectors = { selectAvatar: state => state.github.avatar } /* ------------- Reducers ------------- */ // request the avatar for a user export const request = (state, { username }) => state.merge({ fetching: true, username, avatar: null }) // successful avatar lookup export const success = (state, action) => { const { avatar } = action return state.merge({ fetching: false, error: null, avatar }) } // failed to get the avatar export const failure = (state) => state.merge({ fetching: false, error: true, avatar: null }) /* ------------- Hookup Reducers To Types ------------- */ export const reducer = createReducer(INITIAL_STATE, { [Types.USER_REQUEST]: request, [Types.USER_SUCCESS]: success, [Types.USER_FAILURE]: failure }) ================================================ FILE: boilerplate/App/Redux/NavigationRedux.js ================================================ import AppNavigation from '../Navigation/AppNavigation' export const reducer = (state, action) => { const newState = AppNavigation.router.getStateForAction(action, state) return newState || state } ================================================ FILE: boilerplate/App/Redux/ScreenTrackingMiddleware.js ================================================ import { NavigationActions } from 'react-navigation' // gets the current screen from navigation state const getCurrentRouteName = (navigationState) => { if (!navigationState) { return null } const route = navigationState.routes[navigationState.index] // dive into nested navigators if (route.routes) { return getCurrentRouteName(route) } return route.routeName } const screenTracking = ({ getState }) => next => (action) => { if ( action.type !== NavigationActions.NAVIGATE && action.type !== NavigationActions.BACK ) { return next(action) } const currentScreen = getCurrentRouteName(getState().nav) const result = next(action) const nextScreen = getCurrentRouteName(getState().nav) if (nextScreen !== currentScreen) { try { __DEV__ && console.tron.log(`NAVIGATING ${currentScreen} to ${nextScreen}`) // Example: Analytics.trackEvent('user_navigation', {currentScreen, nextScreen}) } catch (e) { __DEV__ && console.tron.log(e) } } return result } export default screenTracking ================================================ FILE: boilerplate/App/Redux/SearchRedux.js ================================================ import { createReducer, createActions } from 'reduxsauce' import Immutable from 'seamless-immutable' import { filter } from 'ramda' import { startsWith } from 'ramdasauce' const LIST_DATA = ['sausage', 'blubber', 'pencil', 'cloud', 'moon', 'water', 'computer', 'school', 'network', 'hammer', 'walking', 'violently', 'mediocre', 'literature', 'chair', 'two', 'window', 'cords', 'musical', 'zebra', 'xylophone', 'penguin', 'home', 'dog', 'final', 'ink', 'teacher', 'fun', 'website', 'banana', 'uncle', 'softly', 'mega', 'ten', 'awesome', 'attatch', 'blue', 'internet', 'bottle', 'tight', 'zone', 'tomato', 'prison', 'hydro', 'cleaning', 'telivision', 'send', 'frog', 'cup', 'book', 'zooming', 'falling', 'evily', 'gamer', 'lid', 'juice', 'moniter', 'captain', 'bonding', 'loudly', 'thudding', 'guitar', 'shaving', 'hair', 'soccer', 'water', 'racket', 'table', 'late', 'media', 'desktop', 'flipper', 'club', 'flying', 'smooth', 'monster', 'purple', 'guardian', 'bold', 'hyperlink', 'presentation', 'world', 'national', 'comment', 'element', 'magic', 'lion', 'sand', 'crust', 'toast', 'jam', 'hunter', 'forest', 'foraging', 'silently', 'tawesomated', 'joshing', 'pong', 'RANDOM', 'WORD' ] /* ------------- Types and Action Creators ------------- */ const { Types, Creators } = createActions({ search: ['searchTerm'], cancelSearch: null }) export const TemperatureTypes = Types export default Creators /* ------------- Initial State ------------- */ export const INITIAL_STATE = Immutable({ searchTerm: '', searching: false, results: LIST_DATA }) /* ------------- Reducers ------------- */ export const performSearch = (state, { searchTerm }) => { const results = filter(startsWith(searchTerm), LIST_DATA) return state.merge({ searching: true, searchTerm, results }) } export const cancelSearch = (state) => INITIAL_STATE /* ------------- Hookup Reducers To Types ------------- */ export const reducer = createReducer(INITIAL_STATE, { [Types.SEARCH]: performSearch, [Types.CANCEL_SEARCH]: cancelSearch }) ================================================ FILE: boilerplate/App/Redux/StartupRedux.js ================================================ import { createActions } from 'reduxsauce' /* ------------- Types and Action Creators ------------- */ const { Types, Creators } = createActions({ startup: null }) export const StartupTypes = Types export default Creators ================================================ FILE: boilerplate/App/Redux/index.js ================================================ import { combineReducers } from 'redux' import configureStore from './CreateStore' import rootSaga from '../Sagas/' /* ------------- Assemble The Reducers ------------- */ export const reducers = combineReducers({ nav: require('./NavigationRedux').reducer, github: require('./GithubRedux').reducer, search: require('./SearchRedux').reducer }) export default () => { let { store, sagasManager, sagaMiddleware } = configureStore(reducers, rootSaga) if (module.hot) { module.hot.accept(() => { const nextRootReducer = require('./').reducers store.replaceReducer(nextRootReducer) const newYieldedSagas = require('../Sagas').default sagasManager.cancel() sagasManager.done.then(() => { sagasManager = sagaMiddleware(newYieldedSagas) }) }) } return store } ================================================ FILE: boilerplate/App/Sagas/GithubSagas.js ================================================ import { call, put } from 'redux-saga/effects' import { path } from 'ramda' import GithubActions from '../Redux/GithubRedux' export function * getUserAvatar (api, action) { const { username } = action // make the call to the api const response = yield call(api.getUser, username) if (response.ok) { const firstUser = path(['data', 'items'], response)[0] const avatar = firstUser.avatar_url // do data conversion here if needed yield put(GithubActions.userSuccess(avatar)) } else { yield put(GithubActions.userFailure()) } } ================================================ FILE: boilerplate/App/Sagas/StartupSagas.js ================================================ import { put, select } from 'redux-saga/effects' import GithubActions, { GithubSelectors } from '../Redux/GithubRedux' import { is } from 'ramda' // exported to make available for tests export const selectAvatar = GithubSelectors.selectAvatar // process STARTUP actions export function * startup (action) { if (__DEV__ && console.tron) { // straight-up string logging console.tron.log('Hello, I\'m an example of how to log via Reactotron.') // logging an object for better clarity console.tron.log({ message: 'pass objects for better logging', someGeneratorFunction: selectAvatar }) // fully customized! const subObject = { a: 1, b: [1, 2, 3], c: true } subObject.circularDependency = subObject // osnap! console.tron.display({ name: '🔥 IGNITE 🔥', preview: 'You should totally expand this', value: { '💃': 'Welcome to the future!', subObject, someInlineFunction: () => true, someGeneratorFunction: startup, someNormalFunction: selectAvatar } }) } const avatar = yield select(selectAvatar) // only get if we don't have it yet if (!is(String, avatar)) { yield put(GithubActions.userRequest('GantMan')) } } ================================================ FILE: boilerplate/App/Sagas/index.js ================================================ import { takeLatest, all } from 'redux-saga/effects' import API from '../Services/Api' import FixtureAPI from '../Services/FixtureApi' import DebugConfig from '../Config/DebugConfig' /* ------------- Types ------------- */ import { StartupTypes } from '../Redux/StartupRedux' import { GithubTypes } from '../Redux/GithubRedux' /* ------------- Sagas ------------- */ import { startup } from './StartupSagas' import { getUserAvatar } from './GithubSagas' /* ------------- API ------------- */ // The API we use is only used from Sagas, so we create it here and pass along // to the sagas which need it. const api = DebugConfig.useFixtures ? FixtureAPI : API.create() /* ------------- Connect Types To Sagas ------------- */ export default function * root () { yield all([ // some sagas only receive an action takeLatest(StartupTypes.STARTUP, startup), // some sagas receive extra parameters in addition to an action takeLatest(GithubTypes.USER_REQUEST, getUserAvatar, api) ]) } ================================================ FILE: boilerplate/App/Services/Api.js ================================================ // a library to wrap and simplify api calls import apisauce from 'apisauce' // our "constructor" const create = (baseURL = 'https://api.github.com/') => { // ------ // STEP 1 // ------ // // Create and configure an apisauce-based api object. // const api = apisauce.create({ // base URL is read from the "constructor" baseURL, // here are some default headers headers: { 'Cache-Control': 'no-cache' }, // 10 second timeout... timeout: 10000 }) // ------ // STEP 2 // ------ // // Define some functions that call the api. The goal is to provide // a thin wrapper of the api layer providing nicer feeling functions // rather than "get", "post" and friends. // // I generally don't like wrapping the output at this level because // sometimes specific actions need to be take on `403` or `401`, etc. // // Since we can't hide from that, we embrace it by getting out of the // way at this level. // const getRoot = () => api.get('') const getRate = () => api.get('rate_limit') const getUser = (username) => api.get('search/users', {q: username}) // ------ // STEP 3 // ------ // // Return back a collection of functions that we would consider our // interface. Most of the time it'll be just the list of all the // methods in step 2. // // Notice we're not returning back the `api` created in step 1? That's // because it is scoped privately. This is one way to create truly // private scoped goodies in JavaScript. // return { // a list of the API functions from step 2 getRoot, getRate, getUser } } // let's return back our create method as the default. export default { create } ================================================ FILE: boilerplate/App/Services/ExamplesRegistry.js ================================================ import React from 'react' import { Text, View } from 'react-native' import R from 'ramda' import { ApplicationStyles } from '../Themes' import DebugConfig from '../Config/DebugConfig' let globalComponentExamplesRegistry = [] let globalPluginExamplesRegistry = [] export const addComponentExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalComponentExamplesRegistry.push({title, usage}) } // eslint-disable-line export const addPluginExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalPluginExamplesRegistry.push({title, usage}) } // eslint-disable-line const renderComponentExample = (example) => { return ( {example.title} {example.usage.call()} ) } const renderPluginExample = (example) => { return ( {example.title} {example.usage.call()} ) } export const renderComponentExamples = () => R.map(renderComponentExample, globalComponentExamplesRegistry) export const renderPluginExamples = () => R.map(renderPluginExample, globalPluginExamplesRegistry) // Default for readability export default { renderComponentExamples, addComponentExample, renderPluginExamples, addPluginExample } ================================================ FILE: boilerplate/App/Services/FixtureApi.js ================================================ export default { // Functions return fixtures getRoot: () => { return { ok: true, data: require('../Fixtures/root.json') } }, getRate: () => { return { ok: true, data: require('../Fixtures/rateLimit.json') } }, getUser: (username) => { // This fixture only supports gantman or else returns skellock const gantmanData = require('../Fixtures/gantman.json') const skellockData = require('../Fixtures/skellock.json') return { ok: true, data: username.toLowerCase() === 'gantman' ? gantmanData : skellockData } } } ================================================ FILE: boilerplate/App/Themes/ApplicationStyles.js ================================================ import Fonts from './Fonts' import Metrics from './Metrics' import Colors from './Colors' // This file is for a reusable grouping of Theme items. // Similar to an XML fragment layout in Android const ApplicationStyles = { screen: { mainContainer: { flex: 1, backgroundColor: Colors.transparent }, backgroundImage: { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0 }, container: { flex: 1, paddingTop: Metrics.baseMargin, backgroundColor: Colors.transparent }, section: { margin: Metrics.section, padding: Metrics.baseMargin }, sectionText: { ...Fonts.style.normal, paddingVertical: Metrics.doubleBaseMargin, color: Colors.snow, marginVertical: Metrics.smallMargin, textAlign: 'center' }, subtitle: { color: Colors.snow, padding: Metrics.smallMargin, marginBottom: Metrics.smallMargin, marginHorizontal: Metrics.smallMargin }, titleText: { ...Fonts.style.h2, fontSize: 14, color: Colors.text } }, darkLabelContainer: { padding: Metrics.smallMargin, paddingBottom: Metrics.doubleBaseMargin, borderBottomColor: Colors.border, borderBottomWidth: 1, marginBottom: Metrics.baseMargin }, darkLabel: { fontFamily: Fonts.type.bold, color: Colors.snow }, groupContainer: { margin: Metrics.smallMargin, flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center' }, sectionTitle: { ...Fonts.style.h4, color: Colors.coal, backgroundColor: Colors.ricePaper, padding: Metrics.smallMargin, marginTop: Metrics.smallMargin, marginHorizontal: Metrics.baseMargin, borderWidth: 1, borderColor: Colors.ember, alignItems: 'center', textAlign: 'center' } } export default ApplicationStyles ================================================ FILE: boilerplate/App/Themes/Colors.js ================================================ const colors = { background: '#1F0808', clear: 'rgba(0,0,0,0)', facebook: '#3b5998', transparent: 'rgba(0,0,0,0)', silver: '#F7F7F7', steel: '#CCCCCC', error: 'rgba(200, 0, 0, 0.8)', ricePaper: 'rgba(255,255,255, 0.75)', frost: '#D8D8D8', cloud: 'rgba(200,200,200, 0.35)', windowTint: 'rgba(0, 0, 0, 0.4)', panther: '#161616', charcoal: '#595959', coal: '#2d2d2d', bloodOrange: '#fb5f26', snow: 'white', ember: 'rgba(164, 0, 48, 0.5)', fire: '#e73536', drawer: 'rgba(30, 30, 29, 0.95)', eggplant: '#251a34', border: '#483F53', banner: '#5F3E63', text: '#E0D7E5' } export default colors ================================================ FILE: boilerplate/App/Themes/Fonts.js ================================================ const type = { base: 'Avenir-Book', bold: 'Avenir-Black', emphasis: 'HelveticaNeue-Italic' } const size = { h1: 38, h2: 34, h3: 30, h4: 26, h5: 20, h6: 19, input: 18, regular: 17, medium: 14, small: 12, tiny: 8.5 } const style = { h1: { fontFamily: type.base, fontSize: size.h1 }, h2: { fontWeight: 'bold', fontSize: size.h2 }, h3: { fontFamily: type.emphasis, fontSize: size.h3 }, h4: { fontFamily: type.base, fontSize: size.h4 }, h5: { fontFamily: type.base, fontSize: size.h5 }, h6: { fontFamily: type.emphasis, fontSize: size.h6 }, normal: { fontFamily: type.base, fontSize: size.regular }, description: { fontFamily: type.base, fontSize: size.medium } } export default { type, size, style } ================================================ FILE: boilerplate/App/Themes/Images.js ================================================ // leave off @2x/@3x const images = { logo: require('../Images/ir.png'), clearLogo: require('../Images/top_logo.png'), launch: require('../Images/launch-icon.png'), ready: require('../Images/your-app.png'), ignite: require('../Images/ignite_logo.png'), igniteClear: require('../Images/ignite-logo-transparent.png'), tileBg: require('../Images/tile_bg.png'), background: require('../Images/BG.png'), buttonBackground: require('../Images/button-bg.png'), api: require('../Images/Icons/icon-api-testing.png'), components: require('../Images/Icons/icon-components.png'), deviceInfo: require('../Images/Icons/icon-device-information.png'), faq: require('../Images/Icons/faq-icon.png'), home: require('../Images/Icons/icon-home.png'), theme: require('../Images/Icons/icon-theme.png'), usageExamples: require('../Images/Icons/icon-usage-examples.png'), chevronRight: require('../Images/Icons/chevron-right.png'), hamburger: require('../Images/Icons/hamburger.png'), backButton: require('../Images/Icons/back-button.png'), closeButton: require('../Images/Icons/close-button.png') } export default images ================================================ FILE: boilerplate/App/Themes/Metrics.js ================================================ import {Dimensions, Platform} from 'react-native' const { width, height } = Dimensions.get('window') // Used via Metrics.baseMargin const metrics = { marginHorizontal: 10, marginVertical: 10, section: 25, baseMargin: 10, doubleBaseMargin: 20, smallMargin: 5, doubleSection: 50, horizontalLineHeight: 1, screenWidth: width < height ? width : height, screenHeight: width < height ? height : width, navBarHeight: (Platform.OS === 'ios') ? 64 : 54, buttonRadius: 4, icons: { tiny: 15, small: 20, medium: 30, large: 45, xl: 50 }, images: { small: 20, medium: 40, large: 60, logo: 200 } } export default metrics ================================================ FILE: boilerplate/App/Themes/README.md ================================================ ### Themes Folder Application specific themes * Base Styles * Fonts * Metrics * Colors etc. ================================================ FILE: boilerplate/App/Themes/index.js ================================================ import Colors from './Colors' import Fonts from './Fonts' import Metrics from './Metrics' import Images from './Images' import ApplicationStyles from './ApplicationStyles' export { Colors, Fonts, Images, Metrics, ApplicationStyles } ================================================ FILE: boilerplate/App/Transforms/ConvertFromKelvin.js ================================================ export default (kelvin: number) => { const celsius = kelvin - 273.15 const fahrenheit = (celsius * 1.8000) + 32 return Math.round(fahrenheit) } ================================================ FILE: boilerplate/App/Transforms/README.md ================================================ # Transforms A common pattern when working with APIs is to change data to play nice between your app & the API. We've found this to be the case in every project we've worked on. So much so that we're recommending that you create a folder dedicated to these transformations. Transforms are not necessarily a bad thing (although an API might have you transforming more than you'd like). For example, you may: * turn appropriate strings to date objects * convert snake case to camel case * normalize or denormalize things * create lookup tables ================================================ FILE: boilerplate/README.md ================================================ # <%= props.name %> [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) * Standard compliant React Native App Utilizing [Ignite](https://github.com/infinitered/ignite) ## :arrow_up: How to Setup **Step 1:** git clone this repo: **Step 2:** cd to the cloned repo: **Step 3:** Install the Application with `yarn` or `npm i` ## :arrow_forward: How to Run App 1. cd to the repo 2. Run Build for either OS * for iOS * run `npx react-native run-ios` * for Android * Run Genymotion * run `npx react-native run-android` ## :no_entry_sign: Standard Compliant [![js-standard-style](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) This project adheres to Standard. Our CI enforces this, so we suggest you enable linting to keep your project compliant during development. **To Lint on Commit** This is implemented using [husky](https://github.com/typicode/husky). There is no additional setup needed. **Bypass Lint** If you have to bypass lint for a special commit that you will come back and clean (pushing something to a branch etc.) then you can bypass git hooks with adding `--no-verify` to your commit command. **Understanding Linting Errors** The linting rules are from JS Standard and React-Standard. [Regular JS errors can be found with descriptions here](http://eslint.org/docs/rules/), while [React errors and descriptions can be found here](https://github.com/yannickcr/eslint-plugin-react). ## :closed_lock_with_key: Secrets This project uses [react-native-config](https://github.com/luggit/react-native-config) to expose config variables to your javascript code in React Native. You can store API keys and other sensitive information in a `.env` file: ``` API_URL=https://myapi.com GOOGLE_MAPS_API_KEY=abcdefgh ``` and access them from React Native like so: ``` import Secrets from 'react-native-config' Secrets.API_URL // 'https://myapi.com' Secrets.GOOGLE_MAPS_API_KEY // 'abcdefgh' ``` The `.env` file is ignored by git keeping those secrets out of your repo. ### Get started: 1. Copy .env.example to .env 2. Add your config variables 3. Follow instructions at [https://github.com/luggit/react-native-config#setup](https://github.com/luggit/react-native-config#setup) 4. Done! ================================================ FILE: boilerplate/Tests/Components/AlertMessageTest.js ================================================ import 'react-native' import React from 'react' import AlertMessage from '../../App/Components/AlertMessage' import renderer from 'react-test-renderer' test('AlertMessage component renders correctly if show is true', () => { const tree = renderer.create().toJSON() expect(tree).toMatchSnapshot() }) test('AlertMessage component does not render if show is false', () => { const tree = renderer.create().toJSON() expect(tree).toMatchSnapshot() }) test('AlertMessage component renders correctly if backgroundColor prop is set', () => { const tree = renderer.create().toJSON() expect(tree).toMatchSnapshot() }) ================================================ FILE: boilerplate/Tests/Components/DrawerButtonTest.js ================================================ import 'react-native' import React from 'react' import DrawerButton from '../../App/Components/DrawerButton' import { shallow } from 'enzyme' import renderer from 'react-test-renderer' test('DrawerButton component renders correctly', () => { const tree = renderer.create( {}} text='hi' />).toJSON() expect(tree).toMatchSnapshot() }) test('onPress', () => { let i = 0 const onPress = () => i++ const wrapperPress = shallow() expect(wrapperPress.prop('onPress')).toBe(onPress) // uses the right handler expect(i).toBe(0) wrapperPress.simulate('press') expect(i).toBe(1) }) ================================================ FILE: boilerplate/Tests/Components/FullButtonTest.js ================================================ import 'react-native' import React from 'react' import FullButton from '../../App/Components/FullButton' import { shallow } from 'enzyme' import renderer from 'react-test-renderer' test('FullButton component renders correctly', () => { const tree = renderer.create( {}} text='hi' />).toJSON() expect(tree).toMatchSnapshot() }) test('onPress', () => { let i = 0 // i guess i could have used sinon here too... less is more i guess const onPress = () => i++ const wrapperPress = shallow() expect(wrapperPress.prop('onPress')).toBe(onPress) // uses the right handler expect(i).toBe(0) wrapperPress.simulate('press') expect(i).toBe(1) }) ================================================ FILE: boilerplate/Tests/Components/RoundedButtonTest.js ================================================ import 'react-native' import React from 'react' import RoundedButton from '../../App/Components/RoundedButton' import { shallow } from 'enzyme' import renderer from 'react-test-renderer' test('RoundedButton component renders correctly', () => { const tree = renderer.create( {}} text='howdy' />).toJSON() expect(tree).toMatchSnapshot() }) test('RoundedButton component with children renders correctly', () => { const tree = renderer.create( {}}>Howdy).toJSON() expect(tree).toMatchSnapshot() }) test('onPress', () => { let i = 0 // i guess i could have used sinon here too... less is more i guess const onPress = () => i++ const wrapperPress = shallow() expect(wrapperPress.prop('onPress')).toBe(onPress) // uses the right handler expect(i).toBe(0) wrapperPress.simulate('press') expect(i).toBe(1) }) ================================================ FILE: boilerplate/Tests/Redux/GithubReduxTest.js ================================================ import Actions, { reducer, INITIAL_STATE } from '../../App/Redux/GithubRedux' test('request', () => { const username = 'taco' const state = reducer(INITIAL_STATE, Actions.userRequest(username)) expect(state.fetching).toBe(true) expect(state.username).toBe(username) expect(state.avatar).toBeNull() }) test('success', () => { const avatar = 'http://placekitten.com/200/300' const state = reducer(INITIAL_STATE, Actions.userSuccess(avatar)) expect(state.fetching).toBe(false) expect(state.avatar).toBe(avatar) expect(state.error).toBeNull() }) test('failure', () => { const state = reducer(INITIAL_STATE, Actions.userFailure()) expect(state.fetching).toBe(false) expect(state.error).toBe(true) expect(state.avatar).toBeNull() }) ================================================ FILE: boilerplate/Tests/Sagas/GithubSagaTest.js ================================================ import FixtureAPI from '../../App/Services/FixtureApi' import { put, call } from 'redux-saga/effects' import { getUserAvatar } from '../../App/Sagas/GithubSagas' import GithubActions from '../../App/Redux/GithubRedux' import { path } from 'ramda' const stepper = (fn) => (mock) => fn.next(mock).value test('first calls API', () => { const step = stepper(getUserAvatar(FixtureAPI, {username: 'taco'})) // first yield is API expect(step()).toEqual(call(FixtureAPI.getUser, 'taco')) }) test('success path', () => { const response = FixtureAPI.getUser('taco') const step = stepper(getUserAvatar(FixtureAPI, {username: 'taco'})) // first step API step() // Second step successful return const stepResponse = step(response) // Get the avatar Url from the response const firstUser = path(['data', 'items'], response)[0] const avatar = firstUser.avatar_url expect(stepResponse).toEqual(put(GithubActions.userSuccess(avatar))) }) test('failure path', () => { const response = {ok: false} const step = stepper(getUserAvatar(FixtureAPI, {username: 'taco'})) // first step API step() // Second step failed response expect(step(response)).toEqual(put(GithubActions.userFailure())) }) ================================================ FILE: boilerplate/Tests/Sagas/StartupSagaTest.js ================================================ import { select, put } from 'redux-saga/effects' import { selectAvatar, startup } from '../../App/Sagas/StartupSagas' import GithubActions from '../../App/Redux/GithubRedux' const stepper = (fn) => (mock) => fn.next(mock).value test('watches for the right action', () => { const step = stepper(startup()) GithubActions.userRequest('GantMan') expect(step()).toEqual(select(selectAvatar)) expect(step()).toEqual(put(GithubActions.userRequest('GantMan'))) }) ================================================ FILE: boilerplate/Tests/Services/FixtureAPITest.js ================================================ import API from '../../App/Services/Api' import FixtureAPI from '../../App/Services/FixtureApi' import R from 'ramda' test('All fixtures map to actual API', () => { const fixtureKeys = R.keys(FixtureAPI).sort() const apiKeys = R.keys(API.create()) const intersection = R.intersection(fixtureKeys, apiKeys).sort() // There is no difference between the intersection and all fixtures expect(R.equals(fixtureKeys, intersection)).toBe(true) }) test('FixtureAPI getRate returns the right file', () => { const expectedFile = require('../../App/Fixtures/rateLimit.json') expect(FixtureAPI.getRate()).toEqual({ ok: true, data: expectedFile }) }) test('FixtureAPI getUser returns the right file for gantman', () => { const expectedFile = require('../../App/Fixtures/gantman.json') expect(FixtureAPI.getUser('GantMan')).toEqual({ ok: true, data: expectedFile }) }) test('FixtureAPI getUser returns the right file for skellock as default', () => { const expectedFile = require('../../App/Fixtures/skellock.json') expect(FixtureAPI.getUser('Whatever')).toEqual({ ok: true, data: expectedFile }) }) ================================================ FILE: boilerplate/Tests/Setup.js.ejs ================================================ import { configure } from 'enzyme' import Adapter from 'enzyme-adapter-react-16' configure({ adapter: new Adapter() }) // Mock your external modules here if needed <%_ if (props.i18n === 'react-native-i18n') { _%> jest .mock('react-native-i18n', () => { const english = require('../App/I18n/languages/english.json') const keys = require('ramda') const replace = require('ramda') const forEach = require('ramda') return { t: (key, replacements) => { let value = english[key] if (!value) return key if (!replacements) return value forEach((r) => { value = replace(`{{${r}}}`, replacements[r], value) }, keys(replacements)) return value } } }) <%_ } else { _%> // jest // .mock('react-native-device-info', () => { // return { isTablet: jest.fn(() => { return false }) } // }) <%_ } _%> ================================================ FILE: boilerplate/Tests/StoriesTest.js ================================================ import initStoryshots from '@storybook/addon-storyshots' initStoryshots() ================================================ FILE: boilerplate/ignite.json.ejs ================================================ { "createdWith": "<%= props.igniteVersion %>", "boilerplate": "ignite-andross", "examples": "classic", "navigation": "react-navigation", "askToOverwrite": true } ================================================ FILE: boilerplate/index.js.ejs ================================================ import './App/Config/ReactotronConfig' import { AppRegistry } from 'react-native' import App from './App/Containers/App' AppRegistry.registerComponent('<%= props.name %>', () => App) ================================================ FILE: boilerplate/package.json.ejs ================================================ { "version": "0.0.1", "scripts": { "clean": "rm -rf $TMPDIR/react-* && watchman watch-del-all && npm cache clean --force", "clean:android": "cd android/ && ./gradlew clean && cd .. && npx react-native run-android", "newclear": "rm -rf $TMPDIR/react-* && watchman watch-del-all && rm -rf ios/build && rm -rf node_modules/ && npm cache clean --force && npm i", "test:watch": "jest --watch", "updateSnapshot": "jest --updateSnapshot", "coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html", "android:build": "cd android && ./gradlew assembleRelease", "android:install": "cd android && ./gradlew assembleRelease && ./gradlew installRelease", "android:hockeyapp": "cd android && ./gradlew assembleRelease && puck -submit=auto app/build/outputs/apk/app-release.apk", "android:devices": "$ANDROID_HOME/platform-tools/adb devices", "android:logcat": "$ANDROID_HOME/platform-tools/adb logcat *:S ReactNative:V ReactNativeJS:V", "android:shake": "$ANDROID_HOME/platform-tools/adb devices | grep '\\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} $ANDROID_HOME/platform-tools/adb -s {} shell input keyevent 82", "storybook": "storybook start -p 7007" }, "dependencies": { "@react-native-community/async-storage": "^1.11.0", "@react-native-community/masked-view": "^0.1.10", "apisauce": "^1.1.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.2", "format-json": "^1.0.3", "identity-obj-proxy": "^3.0.0", "lodash": "^4.17.17", "prop-types": "^15.7.2", "querystringify": "^2.1.1", "ramda": "^0.27.0", "ramdasauce": "^2.1.3", "react": "16.13.1", "react-native": "0.63.0", "react-native-config": "^0.12.0", "react-native-device-info": "^5.3.0", "react-native-gesture-handler": "1.6.1", "react-native-safe-area-context": "^3.0.7", "react-native-safe-area-view": "^1.1.1", "react-native-screens": "^2.9.0", "react-navigation": "4.0.0", "react-navigation-redux-helpers": "^4.0.1", "react-navigation-stack": "^1.10.3", "react-navigation-tabs": "^2.5.6", "react-redux": "^7.1.3", "redux": "^4.0.4", "redux-saga": "^1.1.3", "reduxsauce": "^1.1.1", "seamless-immutable": "^7.1.4" }, "devDependencies": { "@babel/core": "^7.8.4", "@babel/runtime": "^7.8.4", "@react-native-community/eslint-config": "^1.1.0", "@storybook/addon-actions": "5.0.11", "@storybook/addon-links": "5.0.11", "@storybook/addon-storyshots": "^4.1.11", "@storybook/addons": "^4.1.11", "@storybook/channels": "^4.1.11", "@storybook/react-native": "^4.1.11", "babel-eslint": "7.1.1", "babel-jest": "^25.1.0", "babel-plugin-ignite-ignore-reactotron": "^0.3.0", "eslint": "^6.6.0", "jest": "^25.1.0", "metro-react-native-babel-preset": "^0.59.0", "react-test-renderer": "16.13.1", "reactotron-react-native": "^4.0.2", "reactotron-redux": "^3.1.2", "reactotron-redux-saga": "^4.2.2", "snazzy": "^8.0.0", "standard": "10.0.2" }, "jest": { "testMatch": [ "/Tests/**/*.js", "**/?(*.)(spec|test).js?(x)" ], "testPathIgnorePatterns":[ "/node_modules/", "/Tests/Setup.js" ], "moduleNameMapper": { "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "identity-obj-proxy" }, "transform": { "^.+\\.(js)$": "/node_modules/react-native/jest/preprocessor.js" }, "setupFiles": [ "/Tests/Setup" ], "preset": "react-native" }, "config": {} } ================================================ FILE: boilerplate/storybook/addons.js ================================================ import '@storybook/addon-actions/register' import '@storybook/addon-links/register' ================================================ FILE: boilerplate/storybook/index.js ================================================ import StorybookUI from './storybook' export default StorybookUI ================================================ FILE: boilerplate/storybook/storybook.ejs ================================================ import { AppRegistry } from 'react-native' import { getStorybookUI, configure } from '@storybook/react-native' // import stories configure(() => { require('../App/Components/Stories') }, module) // This assumes that storybook is running on the same host as your RN packager, // to set manually use, e.g. host: 'localhost' option const StorybookUI = getStorybookUI({ port: 7007, onDeviceUI: true }) AppRegistry.registerComponent('<%= props.name %>', () => StorybookUI) export default StorybookUI ================================================ FILE: boilerplate.js ================================================ const options = require('./options') const { mergeDeepRight, pipe, assoc, omit, __ } = require('ramda') const { getReactNativeVersion } = require('./lib/react-native-version') /** * Is Android installed? * * $ANDROID_HOME/tools folder has to exist. * * @param {*} context - The gluegun context. * @returns {boolean} */ const isAndroidInstalled = function(context) { const androidHome = process.env['ANDROID_HOME'] const hasAndroidEnv = !context.strings.isBlank(androidHome) const hasAndroid = hasAndroidEnv && context.filesystem.exists(`${androidHome}/tools`) === 'dir' return Boolean(hasAndroid) } /** * Let's install. * * @param {any} context - The gluegun context. */ async function install(context) { const { filesystem, parameters, ignite, reactNative, print, system, prompt, template } = context const { colors } = print const { red, yellow, bold, gray, blue } = colors const perfStart = new Date().getTime() const name = parameters.first const spinner = print.spin(`using the ${red('Infinite Red')} boilerplate v2 (code name 'Andross')`).succeed() // attempt to install React Native or die trying const useNpm = !ignite.useYarn const rnInstall = await reactNative.install({ name, version: getReactNativeVersion(context), useNpm, }) if (rnInstall.exitCode > 0) process.exit(rnInstall.exitCode) // remove the __tests__ directory and App.js that come with React Native filesystem.remove('__tests__') filesystem.remove('App.js') // copy our App, Tests & storybook directories spinner.text = '▸ copying files' spinner.start() filesystem.copy(`${__dirname}/boilerplate/App`, `${process.cwd()}/App`, { overwrite: true, matching: '!*.ejs' }) filesystem.copy(`${__dirname}/boilerplate/Tests`, `${process.cwd()}/Tests`, { overwrite: true, matching: '!*.ejs' }) filesystem.copy(`${__dirname}/boilerplate/storybook`, `${process.cwd()}/storybook`, { overwrite: true, matching: '!*.ejs' }) filesystem.dir(`${process.cwd()}/ignite`) spinner.stop() // --max, --min, interactive let answers if (parameters.options.max) { answers = options.answers.max } else if (parameters.options.min) { answers = options.answers.min } else { answers = await prompt.ask(options.questions) } // generate some templates spinner.text = '▸ generating files' const templates = [ { template: 'index.js.ejs', target: 'index.js' }, { template: 'README.md', target: 'README.md' }, { template: 'ignite.json.ejs', target: 'ignite/ignite.json' }, { template: '.editorconfig', target: '.editorconfig' }, { template: '.babelrc', target: '.babelrc' }, { template: 'Tests/Setup.js.ejs', target: 'Tests/Setup.js' }, { template: 'storybook/storybook.ejs', target: 'storybook/storybook.js' }, { template: '.env.example', target: '.env.example' } ] const templateProps = { name, igniteVersion: ignite.version, reactNativeVersion: rnInstall.version, vectorIcons: answers['vector-icons'], animatable: answers['animatable'], i18n: answers['i18n'] } await ignite.copyBatch(context, templates, templateProps, { quiet: !parameters.options.debug, directory: `${ignite.ignitePluginPath()}/boilerplate` }) /** * Append to files */ // https://github.com/facebook/react-native/issues/12724 await filesystem.appendAsync('.gitattributes', '*.bat text eol=crlf') filesystem.append('.gitignore', '\n# Misc\n#') filesystem.append('.gitignore', '\n.env\n') // transform our package.json in case we need to replace variables const rawJson = await template.generate({ directory: `${ignite.ignitePluginPath()}/boilerplate`, template: 'package.json.ejs', props: templateProps }) const newPackageJson = JSON.parse(rawJson) // read in the react-native created package.json const currentPackage = filesystem.read('package.json', 'json') // deep merge, lol const newPackage = pipe( assoc('dependencies', mergeDeepRight(currentPackage.dependencies, newPackageJson.dependencies)), assoc('devDependencies', mergeDeepRight(currentPackage.devDependencies, newPackageJson.devDependencies)), assoc('scripts', mergeDeepRight(currentPackage.scripts, newPackageJson.scripts)), mergeDeepRight(__, omit(['dependencies', 'devDependencies', 'scripts'], newPackageJson)) )(currentPackage) // write this out filesystem.write('package.json', newPackage, { jsonIndent: 2 }) spinner.stop() // react native link -- must use spawn & stdio: ignore or it hangs!! :( // spinner.text = `▸ linking native libraries` // spinner.start() // await system.spawn('npx react-native link', { stdio: 'ignore' }) // spinner.stop() // pass long the debug flag if we're running in that mode const debugFlag = parameters.options.debug ? '--debug' : '' // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= // NOTE(steve): I'm re-adding this here because boilerplates now hold permanent files // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= try { // boilerplate adds itself to get plugin.js/generators etc // Could be directory, npm@version, or just npm name. Default to passed in values const boilerplate = parameters.options.b || parameters.options.boilerplate || 'ignite-andross' await system.spawn(`npx ignite-cli add ${boilerplate} ${debugFlag}`, { stdio: 'inherit' }) // now run install of Ignite Plugins await ignite.addModule('react-navigation', { version: '4.0.0' }) await ignite.addModule('react-native-gesture-handler', { version: '1.6.1', link: true }) ignite.patchInFile(`${process.cwd()}/android/app/src/main/java/com/${name.toLowerCase()}/MainActivity.java`, { after: 'import com.facebook.react.ReactActivity;', insert: ` import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactRootView; import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;` }) ignite.patchInFile(`${process.cwd()}/android/app/src/main/java/com/${name.toLowerCase()}/MainActivity.java`, { after: `public class MainActivity extends ReactActivity {`, insert: '\n @Override\n' + ' protected ReactActivityDelegate createReactActivityDelegate() {\n' + ' return new ReactActivityDelegate(this, getMainComponentName()) {\n' + ' @Override\n' + ' protected ReactRootView createRootView() {\n' + ' return new RNGestureHandlerEnabledRootView(MainActivity.this);\n' + ' }\n' + ' };\n' + ' }' }) if (answers['vector-icons'] === 'react-native-vector-icons') { await system.spawn(`npx ignite-cli add vector-icons@1.1.1 ${debugFlag}`, { stdio: 'inherit' }) } if (answers['i18n'] === 'react-native-i18n') { await system.spawn(`npx ignite-cli add i18n@1.2.0 ${debugFlag}`, { stdio: 'inherit' }) } if (answers['animatable'] === 'react-native-animatable') { await system.spawn(`npx ignite-cli add animatable@1.0.2 ${debugFlag}`, { stdio: 'inherit' }) } // dev-screens be installed after vector-icons and animatable so that it can // conditionally patch its PluginExamplesScreen if (answers['dev-screens'] === 'Yes') { await system.spawn(`npx ignite-cli add dev-screens@"2.4.5" ${debugFlag}`, { stdio: 'inherit' }) } if (answers['redux-persist'] === 'Yes') { await system.spawn(`npx ignite-cli add redux-persist@2.0.0 ${debugFlag}`, { stdio: 'inherit' }) } if (parameters.options.lint !== false) { await system.spawn(`npx ignite-cli add standard@1.0.0 ${debugFlag}`, { stdio: 'inherit' }) } } catch (e) { ignite.log(e) throw e } // git configuration const gitExists = await filesystem.exists('./.git') if (!gitExists && !parameters.options['skip-git'] && system.which('git')) { // initial git const spinner = print.spin('configuring git') // TODO: Make husky hooks optional const huskyCmd = '' // `&& node node_modules/husky/bin/install .` await system.run(`git init . && git add . && git commit -m "Initial commit." ${huskyCmd}`) spinner.succeed(`configured git`) } const perfDuration = parseInt((new Date().getTime() - perfStart) / 10) / 100 spinner.succeed(`ignited ${yellow(name)} in ${perfDuration}s`) const androidInfo = isAndroidInstalled(context) ? '' : `\n\nTo run in Android, make sure you've followed the latest react-native setup instructions at https://facebook.github.io/react-native/docs/getting-started.html before using ignite.\nYou won't be able to run ${bold( 'react-native run-android' )} successfully until you have.` const successMessage = ` ${red('Ignite CLI')} ignited ${yellow(name)} in ${gray(`${perfDuration}s`)} To get started: cd ${name} npx react-native run-ios npx react-native run-android${androidInfo} npx ignite-cli --help ${gray( 'Read the walkthrough at https://github.com/infinitered/ignite-andross/blob/master/readme.md#boilerplate-walkthrough' )} ${blue('Need additional help? Join our Slack community at http://community.infinite.red.')} ${bold('Now get cooking! 🍽')} ` print.info(successMessage) } module.exports = { install } ================================================ FILE: commands/generate/component.js ================================================ module.exports = { description: 'Generates a component, styles, and an optional test.', run: async function(toolbox) { // grab some features const { parameters, strings, print, ignite } = toolbox const { pascalCase, isBlank } = strings const config = ignite.loadIgniteConfig() const { tests } = config const options = parameters.options || {} const hasFolder = !isBlank(options.folder) // validation if (isBlank(parameters.first) && !hasFolder) { print.info(`${toolbox.runtime.brand} generate component \n`) print.info('A name is required.') return } let componentPath = hasFolder ? `${options.folder}/${parameters.first || 'index'}` : parameters.first let pathComponents = componentPath.split('/').map(pascalCase) let name = pathComponents.pop() if (name === 'Index') { name = 'index' } const relativePath = pathComponents.length ? pathComponents.join('/') + '/' : '' const props = { name } const jobs = [ { template: 'component.ejs', target: `App/Components/${relativePath}${name}.js` }, { template: 'component-style.ejs', target: `App/Components/${relativePath}Styles/${name}Style.js` }, tests === 'ava' && { template: 'component-test.ejs', target: `Test/Components/${relativePath}${name}Test.js` } ] await ignite.copyBatch(toolbox, jobs, props) } } ================================================ FILE: commands/generate/container.js ================================================ const patterns = require('../../lib/patterns') module.exports = { description: 'Generates a redux smart component.', run: async function(toolbox) { // grab some features const { parameters, strings, print, ignite, filesystem } = toolbox const { pascalCase, isBlank } = strings const config = ignite.loadIgniteConfig() // validation if (isBlank(parameters.first)) { print.info(`${toolbox.runtime.brand} generate container \n`) print.info('A name is required.') return } const name = pascalCase(parameters.first) const props = { name } const jobs = [ { template: 'container.ejs', target: `App/Containers/${name}.js` }, { template: 'container-style.ejs', target: `App/Containers/Styles/${name}Style.js` } ] await ignite.copyBatch(toolbox, jobs, props) // if using `react-navigation` go the extra step // and insert the container into the nav router if (config.navigation === 'react-navigation') { const containerName = name const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.js` const importToAdd = `import ${containerName} from '../Containers/${containerName}'` const routeToAdd = ` ${containerName}: { screen: ${containerName} },` if (!filesystem.exists(appNavFilePath)) { const msg = `No '${appNavFilePath}' file found. Can't insert container.` print.error(msg) process.exit(1) } // insert container import ignite.patchInFile(appNavFilePath, { after: patterns[patterns.constants.PATTERN_IMPORTS], insert: importToAdd }) // insert container route ignite.patchInFile(appNavFilePath, { after: patterns[patterns.constants.PATTERN_ROUTES], insert: routeToAdd }) } else { print.info('Container created, manually add it to your navigation') } } } ================================================ FILE: commands/generate/generate.js ================================================ module.exports = { name: 'generate', alias: ['g'], run: async toolbox => { toolbox.print.printHelp(toolbox) } } ================================================ FILE: commands/generate/list.js ================================================ module.exports = { alias: ['ls', 'listview'], description: 'Generates a screen with a ListView/Flatlist/SectionList + walkthrough.', run: async function(toolbox) { const patterns = require('../../lib/patterns') // grab some features const { print, parameters, strings, ignite, filesystem } = toolbox const { pascalCase, isBlank } = strings const config = ignite.loadIgniteConfig() // validation if (isBlank(parameters.first)) { print.info(`${toolbox.runtime.brand} generate list \n`) print.info('A name is required.') return } const name = pascalCase(parameters.first) const props = { name } // which type of list in code const typeCodeMessage = 'What coding style do you want for your list?' const typeCodeChoices = ['Flatlist (new)', 'Listview (classic)'] // which type of layout? const typeMessage = 'What kind of List would you like to generate?' const typeChoices = ['Row', 'Grid'] // Sections or no? const typeDataMessage = 'How will your data be presented on this list?' const typeDataChoices = ['Single', 'Sectioned'] // Check for parameters to bypass questions let typeCode = parameters.options.codeType let type = parameters.options.type let dataType = parameters.options.dataType // only prompt if type is not defined if (!typeCode) { // as question 1 const codeAnswers = await toolbox.prompt.ask({ name: 'type', type: 'list', message: typeCodeMessage, choices: typeCodeChoices }) typeCode = codeAnswers.type[0] === typeCodeChoices[0] ? 'flatlist' : 'listview' } if (!type) { // ask question 2 const answers = await toolbox.prompt.ask({ name: 'type', type: 'list', message: typeMessage, choices: typeChoices }) type = answers.type[0] === typeDataChoices[0] ? 'row' : 'grid' } if (!dataType) { // ask question 3 const dataAnswers = await toolbox.prompt.ask({ name: 'type', type: 'list', message: typeDataMessage, choices: typeDataChoices }) dataType = dataAnswers.type[0] === typeDataChoices[0] ? 'single' : 'sectioned' } // Sorry the following is so confusing, but so are React Native lists // There are 3 options and therefore 8 possible combinations let componentTemplate = dataType.toLowerCase() === 'sectioned' ? typeCode + '-sections' : typeCode let styleTemplate = '' // Different logic depending on code types if (typeCode === 'flatlist') { /* * The following mess is because FlatList supports numColumns * where SectionList does not. */ if (type.toLowerCase() === 'grid' && dataType.toLowerCase() === 'sectioned') { // grid + section means we need wrap styleTemplate = 'listview-grid-style' } else if (type.toLowerCase() === 'grid') { componentTemplate = componentTemplate + '-grid' // grid + single = no wrap, use columns styleTemplate = 'flatlist-grid-style' } else { // no grids, flatlist basic styleTemplate = 'listview-style' } } else { // listview builder styleTemplate = type.toLowerCase() === 'grid' ? 'listview-grid-style' : 'listview-style' } const jobs = [ { template: `${componentTemplate}.ejs`, target: `App/Containers/${name}.js` }, { template: `${styleTemplate}.ejs`, target: `App/Containers/Styles/${name}Style.js` } ] await ignite.copyBatch(toolbox, jobs, props) // if using `react-navigation` go the extra step // and insert the screen into the nav router if (config.navigation === 'react-navigation') { const screenName = `${name}` const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.js` const importToAdd = `import ${screenName} from '../Containers/${screenName}'` const routeToAdd = ` ${screenName}: { screen: ${screenName} },` if (!filesystem.exists(appNavFilePath)) { const msg = `No '${appNavFilePath}' file found. Can't insert list screen.` print.error(msg) process.exit(1) } // insert list screen import ignite.patchInFile(appNavFilePath, { after: patterns[patterns.constants.PATTERN_IMPORTS], insert: importToAdd }) // insert list screen route ignite.patchInFile(appNavFilePath, { after: patterns[patterns.constants.PATTERN_ROUTES], insert: routeToAdd }) } else { print.info('List screen created, manually add it to your navigation') } } } ================================================ FILE: commands/generate/redux.js ================================================ module.exports = { description: ' Generates a action/creator/reducer set for Redux.', run: async function(toolbox) { // grab some features const { parameters, ignite, strings, print } = toolbox const { isBlank, pascalCase } = strings const config = ignite.loadIgniteConfig() // validation if (isBlank(parameters.first)) { print.info(`${toolbox.runtime.brand} generate redux \n`) print.info('A name is required.') return } const name = pascalCase(parameters.first) const props = { name } const jobs = [{ template: `redux.ejs`, target: `App/Redux/${name}Redux.js` }] if (config.tests) { jobs.push({ template: `redux-test-${config.tests}.ejs`, target: `Tests/Redux/${name}ReduxTest.js` }) } await ignite.copyBatch(toolbox, jobs, props) } } ================================================ FILE: commands/generate/saga.js ================================================ module.exports = { description: 'Generates a saga with an optional test.', run: async function(toolbox) { // grab some features const { parameters, ignite, print, strings } = toolbox const { pascalCase, isBlank } = strings const config = ignite.loadIgniteConfig() const { tests } = config // validation if (isBlank(parameters.first)) { print.info(`${toolbox.runtime.brand} generate saga \n`) print.info('A name is required.') return } const name = pascalCase(parameters.first) const props = { name } const jobs = [{ template: `saga.ejs`, target: `App/Sagas/${name}Sagas.js` }] if (tests) { jobs.push({ template: `saga-test-${tests}.ejs`, target: `Tests/Sagas/${name}SagaTest.js` }) } // make the templates await ignite.copyBatch(toolbox, jobs, props) } } ================================================ FILE: commands/generate/screen.js ================================================ const patterns = require('../../lib/patterns') module.exports = { description: 'Generates an opinionated container.', run: async function(toolbox) { // grab some features const { parameters, print, strings, ignite, filesystem } = toolbox const { pascalCase, isBlank } = strings const config = ignite.loadIgniteConfig() // validation if (isBlank(parameters.first)) { print.info(`${toolbox.runtime.brand} generate screen \n`) print.info('A name is required.') return } const name = pascalCase(parameters.first) const screenName = name.endsWith('Screen') ? name : `${name}Screen` const props = { name: screenName } const jobs = [ { template: `screen.ejs`, target: `App/Containers/${screenName}.js` }, { template: `screen-style.ejs`, target: `App/Containers/Styles/${screenName}Style.js` } ] // make the templates await ignite.copyBatch(toolbox, jobs, props) // if using `react-navigation` go the extra step // and insert the screen into the nav router if (config.navigation === 'react-navigation') { const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.js` const importToAdd = `import ${screenName} from '../Containers/${screenName}'` const routeToAdd = ` ${screenName}: { screen: ${screenName} },` if (!filesystem.exists(appNavFilePath)) { const msg = `No '${appNavFilePath}' file found. Can't insert screen.` print.error(msg) process.exit(1) } // insert screen import ignite.patchInFile(appNavFilePath, { after: patterns[patterns.constants.PATTERN_IMPORTS], insert: importToAdd }) // insert screen route ignite.patchInFile(appNavFilePath, { after: patterns[patterns.constants.PATTERN_ROUTES], insert: routeToAdd }) } else { print.info(`Screen ${screenName} created, manually add it to your navigation`) } } } ================================================ FILE: ignite.json ================================================ { "generators": [ "component", "container", "listview", "list", "redux", "saga", "screen" ] } ================================================ FILE: lib/patterns.js ================================================ const constants = { PATTERN_IMPORTS: 'imports', PATTERN_ROUTES: 'routes' } // [constants.PATTERN_IMPORTS]: `import[\\s\\S]*from\\s+'react-navigation';?`, module.exports = { constants, [constants.PATTERN_IMPORTS]: `import { createAppContainer } from 'react-navigation'`, [constants.PATTERN_ROUTES]: 'const PrimaryNav' } ================================================ FILE: lib/react-native-version.js ================================================ const { pathOr, is } = require('ramda') // the default React Native version for this boilerplate const REACT_NATIVE_VERSION = '0.63.0' // where the version lives under gluegun const pathToVersion = ['parameters', 'options', 'react-native-version'] // accepts the context and returns back the version const getVersionFromContext = pathOr(REACT_NATIVE_VERSION, pathToVersion) /** * Gets the React Native version to use. * * Attempts to read it from the command line, and if not there, falls back * to the version we want for this boilerplate. For example: * * $ npx ignite-cli new Custom --react-native-version 0.61.1 * * @param {*} context - The gluegun context. */ const getReactNativeVersion = (context = {}) => { const version = getVersionFromContext(context) return is(String, version) ? version : REACT_NATIVE_VERSION } module.exports = { REACT_NATIVE_VERSION, getReactNativeVersion } ================================================ FILE: options.js ================================================ /** * The questions to ask during the install process. */ const questions = [ { name: 'dev-screens', message: 'Would you like Ignite Development Screens?', type: 'list', choices: ['No', 'Yes'] }, { name: 'vector-icons', message: 'What vector icon library will you use?', type: 'list', choices: ['none', 'react-native-vector-icons'] }, { name: 'i18n', message: 'What internationalization library will you use?', type: 'list', choices: ['none', 'react-native-i18n'] }, { name: 'animatable', message: 'What animation library will you use?', type: 'list', choices: ['none', 'react-native-animatable'] }, { name: 'redux-persist', message: 'Would you like to include redux-persist?', type: 'list', choices: ['No', 'Yes'] } ] /** * The max preset. */ const max = { 'dev-screens': 'Yes', 'vector-icons': 'react-native-vector-icons', i18n: 'react-native-i18n', animatable: 'react-native-animatable', 'redux-persist': 'Yes' } /** * The min preset. */ const min = { 'dev-screens': 'No', 'vector-icons': 'none', i18n: 'none', animatable: 'none', 'redux-persist': 'No' } module.exports = { questions, answers: { min, max } } ================================================ FILE: package.json ================================================ { "name": "ignite-andross", "description": "Infinite Red's hot boilerplate for React Native.", "license": "MIT", "repository": "infinitered/ignite-andross", "homepage": "https://github.com/infinitered/ignite-andross", "version": "4.3.0", "files": [ "boilerplate", "commands", "lib", "templates", "boilerplate.js", "ignite.json", "options.js", "plugin.js", "readme.md", "screenExamples.js" ], "author": { "name": "Infinite Red", "email": "npm@infinite.red", "url": "https://github.com/infinitered/ignite-andross" }, "scripts": { "lint": "standard", "test": "jest", "watch": "jest --runInBand --watch", "coverage": "jest --runInBand --coverage", "ci:test": "yarn test", "ci:publish": "yarn semantic-release", "semantic-release": "semantic-release" }, "standard": { "parser": "babel-eslint", "globals": [ "__DEV__", "jasmine", "beforeAll", "afterAll", "test", "expect", "describe" ] }, "prettier": { "semi": false, "singleQuote": true, "printWidth": 120 }, "release": { "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm", "@semantic-release/github", [ "@semantic-release/git", { "assets": "package.json", "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ] ] }, "devDependencies": { "@semantic-release/git": "^7.0.5", "babel-eslint": "^7.1.1", "fs-jetpack": "^1.0.0", "jest": "^20.0.4", "np": "^2.15.0", "semantic-release": "^15.12.2", "sinon": "^2.3.1", "socks": "^2.1.6", "standard": "^10.0.2", "tempy": "^0.1.0" }, "dependencies": { "ramda": "^0.26.1" } } ================================================ FILE: plugin.js ================================================ const screenExamples = require('./screenExamples') /** * Add the plugin. * * @param {any} context - The gluegun context. */ async function add (context) { await screenExamples.add(context) } /** * Remove the plugin. * * @param {any} context - The gluegun context. */ async function remove (context) { await screenExamples.remove(context) } module.exports = { add, remove } ================================================ FILE: readme.md ================================================

logo

# Ignite "Andross" Boilerplate ## Why is this archived? We really appreciate all the community support in the years since we first released ignite-andross. Our focus has shifted to the latest version of [Ignite](https://github.com/infinitered/ignite), which includes the latest version of our boilerplate. Feel free to fork this library and continue on its legacy if you want. NOTE: This repo has been renamed from ignite-ir-boilerplate-andross to ignite-andross. Although web traffic and git operations for the previous name will be redirected, we recommend you update any links and git urls for this repo. ## The original tried and true boilerplate for [Infinite Red](https://infinite.red)'s React Native apps Currently includes: * React Native 0.63 * React Navigation 4.0.0 * Redux * Redux Sagas * And more! ## Quick Start When you've installed the [Ignite CLI](https://github.com/infinitered/ignite), you can get started with this boilerplate like this: ```sh npx ignite-cli new MyLatestCreation ``` By default we'll ask you to choose which boilerplate you'd like. If you just want to use this one you can specify it with `--boilerplate` or `-b`: ```sh npx ignite-cli new MyLatestCreation --boilerplate andross ``` You can also change the React Native version; just keep in mind, we may not have tested this just yet. ```sh npx ignite-cli new MyLatestCreation --react-native-version 0.99.0-rc.2 ``` By default we'll ask you some questions during install as to which features you'd like. If you just want them all, you can skip the questions: ```sh npx ignite-cli new MyLatestCreation --max ``` If you want very few of these extras: ```sh npx ignite-cli new MyLatestCreation --min ``` ## Boilerplate walkthrough Your `App` folder is where most of the goodies are found in an Ignite Next app. Let's walk through them in more detail. Start with `Containers/App.js` (described below) and work your way down the walkthrough in order. ### Containers Containers are (mostly) full screens, although they can be sections of screens or application containers. * `App.js` - your main application. We create a Redux store and configure it here * `RootContainer.js` - main view of your application. Contains your status bar and navigation component * `LaunchScreen.js` - this is the first screen shown in your application. It's loaded into the Navigation component * `Styles` - styling for each of the above containers and screens To generate a new Container or Screen you can use the following generator commands: * `npx ignite-cli g container New` - Will create a `New.js` and also a `Styles/NewStyle.js`. * `npx ignite-cli g list New` - The same as the `container` command, but it will give you a walkthrough to generate a ListView screen. Allowing you to even pick `FlatList` or not, grid, and some other options. * `npx ignite-cli g screen New` - Will create a `NewScreen.js` and also a `Styles/NewScreenStyle.js`. Important to mention that the `screen` generator will add the `Screen` on the file/class name to make easier to identify. Those commands will also add the new container to the navigations file. ### Navigation Your primary and other navigation components reside here. * `AppNavigation.js` - loads in your initial screen and creates your menu(s) in a StackNavigation * `Styles` - styling for the navigation * `ReduxNavigation.js` - This file contains the core navigation of your application. If you ever change your launch screen, make sure to change it also at `if (nav.routes.length === 1 && (nav.routes[0].routeName === 'LaunchScreen')) {`, otherwise you may encounter navigation problems with the Android back button! ### Components React components go here...pretty self-explanatory. We won't go through each in detail -- open each file to read the comments and view the code. To generate a new Component you can use the following generator commands: * `npx ignite-cli g component New` - Will create a `New.js` and also a `Styles/NewStyle.js`. * `npx ignite-cli g component path/New` - The same as above, but will use a relative path * `npx ignite-cli g component --folder path` - An alternative to `npx ignite-cli g component path/index` * `npx ignite-cli g component --folder path new ` - An alternative to `npx ignite-cli g component relativePath/New` ### Storybook [Storybook](https://storybook.js.org/) has been setup to show off components in the different states. Storybook is a great way to develop and test components outside of use in your app. Simply run `npm run storybook` to get started. All stores are contained in the `*.story.js` files along side the components. ### Themes Styling themes used throughout your app styles. * `ApplicationStyles.js` - app-wide styles * `Colors.js` - defined colors for your app * `Fonts.js` - defined fonts for your app * `Images.js` - loads and caches images used in your app * `Metrics.js` - useful measurements of things like navBarHeight ### Config Initialize and configure things here. * `AppConfig.js` - simple React Native configuration here * `DebugConfig.js` - define how you want your debug environment to act * `ReactotronConfig.js` - configures [Reactotron](https://github.com/infinitered/reactotron) in your project (Note: this [will be extracted](https://github.com/infinitered/ignite/issues/779) into a plugin in the future) * `ReduxPersist.js` - configures Redux Persist (Note: this [will be extracted](https://github.com/infinitered/ignite/issues/780) into a plugin in the future) ### Fixtures Contains json files that mimic API responses for quicker development. These are used by the `Services/FixtureApi.js` object to mock API responses. ### Redux, Sagas Contains a preconfigured Redux and Redux-Sagas setup. Review each file carefully to see how Redux interacts with your application. Here again we have generators to help you out. You just have to use one of the following: * `npx ignite-cli g redux Amazing` - Will generate and link the redux for `Amazing`. * `npx ignite-cli g saga Amazing` - The same as above, but for the Sagas You can read more about Redux and Redux Sagas in these blog posts: * [Using redux-saga To Simplify Your Growing React Native Codebase](https://shift.infinite.red/using-redux-saga-to-simplify-your-growing-react-native-codebase-2b8036f650de) * [A Tour of React Native — Part 2: Redux & Friends](https://shift.infinite.red/a-tour-of-react-native-part-2-redux-friends-4fed022aaa1e) ### Services Contains your API service and other important utilities for your application. * `Api.js` - main API service, giving you an interface to communicate with your back end * `ExamplesRegistry.js` - lets you view component and Ignite plugin examples in your app * `FixtureApi.js` - mocks your API service, making it faster to develop early on in your app * `ImmutablePersistenceTransform.js` - part of the redux-persist implementation (will be removed) * `RehydrationServices.js` - part of the redux-persist implementation (will be removed) ### Lib We recommend using this folder for modules that can be extracted into their own NPM packages at some point. ### Images Contains actual images (usually png) used in your application. ### Transforms Helpers for transforming data between API and your application and vice versa. An example is provided that you can look at to see how it works. ### Tests This folder (located as a sibling to `App`) contains sample Jest snapshot and unit tests for your application. If you would like to have the `npx ignite-cli generate` command include the generation of tests when available, add `"tests": "jest"` or `"tests": "ava"` to `./ignite/ignite.json`, depending on the test runner you are using. **Previous Boilerplates** * [2016 aka Ignite 1.0](https://github.com/infinitered/ignite-ir-boilerplate-2016) ## Premium Support [Ignite CLI](https://infinite.red/ignite) and [Ignite Andross](https://github.com/infinitered/ignite-andross), as open source projects, are free to use and always will be. [Infinite Red](https://infinite.red/) offers premium Ignite CLI support and general mobile app design/development services. Email us at [hello@infinite.red](mailto:hello@infinite.red) to get in touch with us for more details. ================================================ FILE: screenExamples.js ================================================ const screenExamples = [ { title: 'Row Example', screen: 'examples/RowExample.js.ejs', ancillary: ['examples/Styles/RowExampleStyle.js.ejs'] }, { title: 'Grid Example', screen: 'examples/GridExample.js.ejs', ancillary: ['examples/Styles/GridExampleStyle.js.ejs'] }, { title: 'Sections Example', screen: 'examples/SectionExample.js.ejs', ancillary: ['examples/Styles/SectionExampleStyle.js.ejs'] } ] /** * Adds the screen examples. * * @param {any} context The gluegun context. */ async function add (context) { // examples of generated screens await context.ignite.addPluginScreenExamples(screenExamples) } /** * Removes the screen examples. * * @param {any} context The gluegun context. */ async function remove (context) { // remove screens await context.ignite.removePluginScreenExamples(screenExamples) } module.exports = { add, remove } ================================================ FILE: templates/component-style.ejs ================================================ import { StyleSheet } from 'react-native' export default StyleSheet.create({ container: { flex: 1 } }) ================================================ FILE: templates/component-test.ejs ================================================ import test from 'ava' import React from 'react' import { shallow } from 'enzyme' import <%= props.name %> from '../../App/Components/<%= props.name %>' const wrapper = shallow(<<%= props.name %> />) test('component exists', t => { t.is(wrapper.length, 1) // exists }) test('component structure', t => { t.is(wrapper.name(), 'View') // the right root component t.is(wrapper.children().length, 1) // has 1 child t.is(wrapper.children().first().name(), 'Text') // that child is Text t.true(wrapper.children().first().containsMatchingElement('<%= props.name %> Component')) // That the Component Text is included }) // test('some other things here', t => { // const wrapper = shallow( ) // You can add in props as shown above, or use the constant wrapper declared // at the top of the file. // }) ================================================ FILE: templates/component.ejs ================================================ import React from 'react' // import PropTypes from 'prop-types'; import { View, Text } from 'react-native' import styles from './Styles/<%= props.name %>Style' const <%= props.name %> = () => { return ( <%= props.name %> Component ) } // // Prop type warnings // <%= props.name %>.propTypes = { // someProperty: PropTypes.object, // someSetting: PropTypes.bool.isRequired, // } // // // Defaults for props // <%= props.name %>.defaultProps = { // someSetting: false // } export default <%= props.name %> ================================================ FILE: templates/container-style.ejs ================================================ import { StyleSheet } from 'react-native' import { Colors, Metrics } from '../../Themes/' export default StyleSheet.create({ container: { flex: 1, marginTop: Metrics.navBarHeight, backgroundColor: Colors.background } }) ================================================ FILE: templates/container.ejs ================================================ import React from 'react' import { ScrollView, Text } from 'react-native' import { connect } from 'react-redux' // Add Actions - replace 'Your' with whatever your reducer is called :) // import YourActions from '../Redux/YourRedux' // Styles import styles from './Styles/<%= props.name %>Style' const <%= props.name %> = () => { return ( <%= props.name %> Container ) } const mapStateToProps = (state) => { return { } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: templates/examples/GridExample.js.ejs ================================================ import React, { Component } from 'react' import { View, Text, ListView } from 'react-native' import { connect } from 'react-redux' // For empty lists // import AlertMessage from '../Components/AlertMessage' // Styles import styles from './Styles/GridExampleStyle' class GridExample extends Component { constructor (props) { super(props) // If you need scroll to bottom, consider http://bit.ly/2bMQ2BZ /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ const dataObjects = [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'} ] /* *********************************************************** * STEP 2 * Teach datasource how to detect if rows are different * Make this function fast! Perhaps something like: * (r1, r2) => r1.id !== r2.id} *************************************************************/ const rowHasChanged = (r1, r2) => r1 !== r2 // DataSource configured const ds = new ListView.DataSource({rowHasChanged}) // Datasource is always in state this.state = { dataSource: ds.cloneWithRows(dataObjects) } } /* *********************************************************** * STEP 3 * `_renderRow` function -How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ _renderRow (rowData) { return ( {rowData.title} {rowData.description} ) } /* *********************************************************** * STEP 4 * If your datasource is driven by Redux, you'll need to * reset it when new data arrives. * DO NOT! place `cloneWithRows` inside of render, since render * is called very often, and should remain fast! Just replace * state's datasource on newProps. * * e.g. componentWillReceiveProps (newProps) { if (newProps.someData) { this.setState(prevState => ({ dataSource: prevState.dataSource.cloneWithRows(newProps.someData) })) } } *************************************************************/ // Used for friendly AlertMessage // returns true if the dataSource is empty _noRowData () { return this.state.dataSource.getRowCount() === 0 } // Render a footer. _renderFooter = () => { return ( - Footer - ) } render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(GridExample) ================================================ FILE: templates/examples/RowExample.js.ejs ================================================ import React, { Component } from 'react' import { View, Text, ListView } from 'react-native' import { connect } from 'react-redux' // Styles import styles from './Styles/RowExampleStyle' class RowExample extends Component { constructor (props) { super(props) // If you need scroll to bottom, consider http://bit.ly/2bMQ2BZ /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ const dataObjects = [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'} ] /* *********************************************************** * STEP 2 * Teach datasource how to detect if rows are different * Make this function fast! Perhaps something like: * (r1, r2) => r1.id !== r2.id} *************************************************************/ const rowHasChanged = (r1, r2) => r1 !== r2 // DataSource configured const ds = new ListView.DataSource({rowHasChanged}) // Datasource is always in state this.state = { dataSource: ds.cloneWithRows(dataObjects) } } /* *********************************************************** * STEP 3 * `_renderRow` function -How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ _renderRow (rowData) { return ( {rowData.title} {rowData.description} ) } /* *********************************************************** * STEP 4 * If your datasource is driven by Redux, you'll need to * reset it when new data arrives. * DO NOT! place `cloneWithRows` inside of render, since render * is called very often, and should remain fast! Just replace * state's datasource on newProps. * * e.g. componentWillReceiveProps (newProps) { if (newProps.someData) { this.setState(prevState => ({ dataSource: prevState.dataSource.cloneWithRows(newProps.someData) })) } } *************************************************************/ // Used for friendly AlertMessage // returns true if the dataSource is empty _noRowData () { return this.state.dataSource.getRowCount() === 0 } // Render a footer. _renderFooter = () => { return ( - Footer - ) } render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(RowExample) ================================================ FILE: templates/examples/SectionExample.js.ejs ================================================ import React, { Component } from 'react' import { View, ListView, Text } from 'react-native' import { connect } from 'react-redux' // Styles import styles from './Styles/SectionExampleStyle' class ListviewSectionsExample extends Component { constructor (props) { super(props) /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ const dataObjects = { first: [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'}, {title: 'Eighth Title', description: 'Eighth Description'}, {title: 'Ninth Title', description: 'Ninth Description'}, {title: 'Tenth Title', description: 'Tenth Description'} ], second: [ {title: 'Eleventh Title', description: 'Eleventh Description'}, {title: '12th Title', description: '12th Description'}, {title: '13th Title', description: '13th Description'}, {title: '14th Title', description: '14th Description'}, {title: '15th Title', description: '15th Description'}, {title: '16th Title', description: '16th Description'}, {title: '17th Title', description: '17th Description'}, {title: '18th Title', description: '18th Description'}, {title: '19th Title', description: '19th Description'}, {title: '20th Title', description: '20th Description'}, {title: 'BLACKJACK!', description: 'BLACKJACK! Description'} ] } /* *********************************************************** * STEP 2 * Teach datasource how to detect if rows are different * Make this function fast! Perhaps something like: * (r1, r2) => r1.id !== r2.id} * The same goes for sectionHeaderHasChanged *************************************************************/ const rowHasChanged = (r1, r2) => r1 !== r2 const sectionHeaderHasChanged = (s1, s2) => s1 !== s2 // DataSource configured const ds = new ListView.DataSource({rowHasChanged, sectionHeaderHasChanged}) // Datasource is always in state this.state = { dataSource: ds.cloneWithRowsAndSections(dataObjects) } } /* *********************************************************** * STEP 3 * `_renderRow` function -How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ _renderRow (rowData, sectionID) { // You can condition on sectionID (key as string), for different cells // in different sections return ( Section {sectionID} - {rowData.title} {rowData.description} ) } /* *********************************************************** * STEP 4 * If your datasource is driven by Redux, you'll need to * reset it when new data arrives. * DO NOT! place `cloneWithRowsAndSections` inside of render, since render * is called very often, and should remain fast! Just replace * state's datasource on newProps. * * e.g. componentWillReceiveProps (newProps) { if (newProps.someData) { this.setState(prevState => ({ dataSource: prevState.dataSource.cloneWithRowsAndSections(newProps.someData) })) } } *************************************************************/ // Used for friendly AlertMessage // returns true if the dataSource is empty _noRowData () { return this.state.dataSource.getRowCount() === 0 } _renderHeader (data, sectionID) { switch (sectionID) { case 'first': return First Section default: return Second Section } } render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(ListviewSectionsExample) ================================================ FILE: templates/examples/Styles/GridExampleStyle.js.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles, Metrics, Colors } from '../../../../../DevScreens/DevTheme/' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { flex: 1, marginTop: Metrics.navBarHeight, backgroundColor: Colors.background }, row: { width: 100, height: 100, justifyContent: 'center', alignItems: 'center', margin: Metrics.baseMargin, backgroundColor: Colors.fire, borderRadius: Metrics.smallMargin }, boldLabel: { fontWeight: 'bold', alignSelf: 'center', color: Colors.snow, textAlign: 'center', marginBottom: Metrics.smallMargin }, label: { alignSelf: 'center', color: Colors.snow, textAlign: 'center' }, listContent: { justifyContent: 'space-around', flexDirection: 'row', flexWrap: 'wrap' } }) ================================================ FILE: templates/examples/Styles/RowExampleStyle.js.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles, Metrics, Colors } from '../../../../../DevScreens/DevTheme/' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { flex: 1, marginTop: Metrics.navBarHeight, backgroundColor: Colors.background }, row: { flex: 1, backgroundColor: Colors.fire, marginVertical: Metrics.smallMargin, justifyContent: 'center' }, boldLabel: { fontWeight: 'bold', alignSelf: 'center', color: Colors.snow, textAlign: 'center', marginBottom: Metrics.smallMargin }, label: { textAlign: 'center', color: Colors.snow }, listContent: { marginTop: Metrics.baseMargin } }) ================================================ FILE: templates/examples/Styles/SectionExampleStyle.js.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles, Metrics, Colors } from '../../../../../DevScreens/DevTheme/' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { flex: 1, marginTop: Metrics.navBarHeight, backgroundColor: Colors.background }, row: { flex: 1, backgroundColor: Colors.fire, marginVertical: Metrics.smallMargin, justifyContent: 'center' }, boldLabel: { fontWeight: 'bold', alignSelf: 'center', color: Colors.snow, textAlign: 'center', marginBottom: Metrics.smallMargin }, label: { textAlign: 'center', color: Colors.snow }, listContent: { marginTop: Metrics.baseMargin } }) ================================================ FILE: templates/flatlist-grid-style.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles, Metrics, Colors } from '../../Themes' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { flex: 1, backgroundColor: Colors.background }, row: { flex: 1, backgroundColor: Colors.fire, marginVertical: Metrics.smallMargin, justifyContent: 'center', margin: 10, padding: 5, paddingVertical: 10, borderRadius: Metrics.smallMargin }, boldLabel: { fontWeight: 'bold', alignSelf: 'center', color: Colors.snow, textAlign: 'center', marginBottom: Metrics.smallMargin }, label: { textAlign: 'center', color: Colors.snow }, listContent: { marginTop: Metrics.baseMargin } }) ================================================ FILE: templates/flatlist-grid.ejs ================================================ import React from 'react' import { View, Text, FlatList } from 'react-native' import { connect } from 'react-redux' // More info here: https://facebook.github.io/react-native/docs/flatlist.html // Styles import styles from './Styles/<%= props.name %>Style' class <%= props.name %> extends React.PureComponent { /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ state = { dataObjects: [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'} ] } /* *********************************************************** * STEP 2 * `renderRow` function. How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ renderRow ({item}) { return ( {item.title} {item.description} ) } /* *********************************************************** * STEP 3 * Consider the configurations we've set below. Customize them * to your liking! Each with some friendly advice. *************************************************************/ // Render a header? renderHeader = () => - Header - // Render a footer? renderFooter = () => - Footer - // Show this when data is empty renderEmpty = () => - Nothing to See Here - renderSeparator = () => - ~~~~~ - // The default function if no Key is provided is index // an identifiable key is important if you plan on // item reordering. Otherwise index is fine keyExtractor = (item, index) => `${index}` // How many items should be kept im memory as we scroll? oneScreensWorth = 20 // extraData is for anything that is not indicated in data // for instance, if you kept "favorites" in `this.state.favs` // pass that in, so changes in favorites will cause a re-render // and your renderItem will have access to change depending on state // e.g. `extraData`={this.state.favs} // Optimize your list if the height of each item can be calculated // by supplying a constant height, there is no need to measure each // item after it renders. This can save significant time for lists // of a size 100+ // e.g. itemLayout={(data, index) => ( // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} // )} render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: templates/flatlist-sections.ejs ================================================ import React from 'react' import { View, SectionList, Text } from 'react-native' import { connect } from 'react-redux' // More info here: https://facebook.github.io/react-native/docs/sectionlist.html // Styles import styles from './Styles/<%= props.name %>Style' class <%= props.name %> extends React.PureComponent { /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ state = { data: [ { key: 'First', data: [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'}, {title: 'Eighth Title', description: 'Eighth Description'}, {title: 'Ninth Title', description: 'Ninth Description'}, {title: 'Tenth Title', description: 'Tenth Description'} ] }, { key: 'Second', data: [ {title: 'Eleventh Title', description: 'Eleventh Description'}, {title: '12th Title', description: '12th Description'}, {title: '13th Title', description: '13th Description'}, {title: '14th Title', description: '14th Description'}, {title: '15th Title', description: '15th Description'}, {title: '16th Title', description: '16th Description'}, {title: '17th Title', description: '17th Description'}, {title: '18th Title', description: '18th Description'}, {title: '19th Title', description: '19th Description'}, {title: '20th Title', description: '20th Description'}, {title: 'BLACKJACK!', description: 'BLACKJACK! Description'} ] } ] } /* *********************************************************** * STEP 3 * `renderItem` function - How each cell should be rendered * It's our best practice to place a single component here: * * e.g. * return * * For sections with different cells (heterogeneous lists), you can do branch * logic here based on section.key OR at the data level, you can provide * `renderItem` functions in each section. * * Note: You can remove section/separator functions and jam them in here *************************************************************/ renderItem ({section, item}) { return ( Section {section.key} - {item.title} {item.description} ) } // Conditional branching for section headers, also see step 3 renderSectionHeader ({section}) { switch (section.key) { case 'First': return First Section default: return Second Section } } /* *********************************************************** * STEP 2 * Consider the configurations we've set below. Customize them * to your liking! Each with some friendly advice. * * Removing a function here will make SectionList use default *************************************************************/ // Render a header? renderHeader = () => - Full List Header - // Render a footer? renderFooter = () => - Full List Footer - // Does each section need a footer? renderSectionFooter = () => END SECTION!!!! // Show this when data is empty renderEmpty = () => - Nothing to See Here - renderSeparator = () => - ~~~~~ - renderSectionSeparator = () => \/\/\/\/\/\/\/\/ // The default function if no Key is provided is index // an identifiable key is important if you plan on // item reordering. Otherwise index is fine keyExtractor = (item, index) => `${index}` // How many items should be kept im memory as we scroll? oneScreensWorth = 20 // extraData is for anything that is not indicated in data // for instance, if you kept "favorites" in `this.state.favs` // pass that in, so changes in favorites will cause a re-render // and your renderItem will have access to change depending on state // e.g. `extraData`={this.state.favs} // Optimize your list if the height of each item can be calculated // by supplying a constant height, there is no need to measure each // item after it renders. This can save significant time for lists // of a size 100+ // e.g. itemLayout={(data, index) => ( // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} // )} render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: templates/flatlist.ejs ================================================ import React from 'react' import { View, Text, FlatList } from 'react-native' import { connect } from 'react-redux' // More info here: https://facebook.github.io/react-native/docs/flatlist.html // Styles import styles from './Styles/<%= props.name %>Style' class <%= props.name %> extends React.PureComponent { /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ state = { dataObjects: [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'} ] } /* *********************************************************** * STEP 2 * `renderRow` function. How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ renderRow ({item}) { return ( {item.title} {item.description} ) } /* *********************************************************** * STEP 3 * Consider the configurations we've set below. Customize them * to your liking! Each with some friendly advice. *************************************************************/ // Render a header? renderHeader = () => - Header - // Render a footer? renderFooter = () => - Footer - // Show this when data is empty renderEmpty = () => - Nothing to See Here - renderSeparator = () => - ~~~~~ - // The default function if no Key is provided is index // an identifiable key is important if you plan on // item reordering. Otherwise index is fine keyExtractor = (item, index) => `${index}` // How many items should be kept im memory as we scroll? oneScreensWorth = 20 // extraData is for anything that is not indicated in data // for instance, if you kept "favorites" in `this.state.favs` // pass that in, so changes in favorites will cause a re-render // and your renderItem will have access to change depending on state // e.g. `extraData`={this.state.favs} // Optimize your list if the height of each item can be calculated // by supplying a constant height, there is no need to measure each // item after it renders. This can save significant time for lists // of a size 100+ // e.g. itemLayout={(data, index) => ( // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} // )} render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: templates/listview-grid-style.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles, Metrics, Colors } from '../../Themes' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { flex: 1, backgroundColor: Colors.background }, row: { width: 100, height: 100, justifyContent: 'center', alignItems: 'center', margin: Metrics.baseMargin, backgroundColor: Colors.fire, borderRadius: Metrics.smallMargin }, sectionHeader: { paddingTop: Metrics.doubleBaseMargin, width: Metrics.screenWidth, alignSelf: 'center', margin: Metrics.baseMargin, backgroundColor: Colors.background }, boldLabel: { fontWeight: 'bold', alignSelf: 'center', color: Colors.snow, textAlign: 'center', marginBottom: Metrics.smallMargin }, label: { alignSelf: 'center', color: Colors.snow, textAlign: 'center' }, listContent: { justifyContent: 'space-around', flexDirection: 'row', flexWrap: 'wrap' } }) ================================================ FILE: templates/listview-sections.ejs ================================================ import React, { Component } from 'react' import { View, ListView, Text } from 'react-native' import { connect } from 'react-redux' // For empty lists // import AlertMessage from '../Components/AlertMessage' // Styles import styles from './Styles/<%= props.name %>Style' class <%= props.name %> extends Component { constructor (props) { super(props) /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ const dataObjects = { first: [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'}, {title: 'Eighth Title', description: 'Eighth Description'}, {title: 'Ninth Title', description: 'Ninth Description'}, {title: 'Tenth Title', description: 'Tenth Description'} ], second: [ {title: 'Eleventh Title', description: 'Eleventh Description'}, {title: '12th Title', description: '12th Description'}, {title: '13th Title', description: '13th Description'}, {title: '14th Title', description: '14th Description'}, {title: '15th Title', description: '15th Description'}, {title: '16th Title', description: '16th Description'}, {title: '17th Title', description: '17th Description'}, {title: '18th Title', description: '18th Description'}, {title: '19th Title', description: '19th Description'}, {title: '20th Title', description: '20th Description'}, {title: 'BLACKJACK!', description: 'BLACKJACK! Description'} ] } /* *********************************************************** * STEP 2 * Teach datasource how to detect if rows are different * Make this function fast! Perhaps something like: * (r1, r2) => r1.id !== r2.id} * The same goes for sectionHeaderHasChanged *************************************************************/ const rowHasChanged = (r1, r2) => r1 !== r2 const sectionHeaderHasChanged = (s1, s2) => s1 !== s2 // DataSource configured const ds = new ListView.DataSource({rowHasChanged, sectionHeaderHasChanged}) // Datasource is always in state this.state = { dataSource: ds.cloneWithRowsAndSections(dataObjects) } } /* *********************************************************** * STEP 3 * `renderRow` function -How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ renderRow (rowData, sectionID) { // You can condition on sectionID (key as string), for different cells // in different sections return ( Section {sectionID} - {rowData.title} {rowData.description} ) } /* *********************************************************** * STEP 4 * If your datasource is driven by Redux, you'll need to * reset it when new data arrives. * DO NOT! place `cloneWithRowsAndSections` inside of render, since render * is called very often, and should remain fast! Just replace * state's datasource on newProps. * * e.g. componentWillReceiveProps (newProps) { if (newProps.someData) { this.setState(prevState => ({ dataSource: prevState.dataSource.cloneWithRowsAndSections(newProps.someData) })) } } *************************************************************/ // Used for friendly AlertMessage // returns true if the dataSource is empty noRowData () { return this.state.dataSource.getRowCount() === 0 } renderHeader (data, sectionID) { switch (sectionID) { case 'first': return First Section default: return Second Section } } render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: templates/listview-style.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles, Metrics, Colors } from '../../Themes' export default StyleSheet.create({ ...ApplicationStyles.screen, container: { flex: 1, backgroundColor: Colors.background }, row: { flex: 1, backgroundColor: Colors.fire, marginVertical: Metrics.smallMargin, justifyContent: 'center' }, boldLabel: { fontWeight: 'bold', alignSelf: 'center', color: Colors.snow, textAlign: 'center', marginBottom: Metrics.smallMargin }, label: { textAlign: 'center', color: Colors.snow }, listContent: { marginTop: Metrics.baseMargin } }) ================================================ FILE: templates/listview.ejs ================================================ import React, { Component } from 'react' import { View, Text, ListView } from 'react-native' import { connect } from 'react-redux' // For empty lists // import AlertMessage from '../Components/AlertMessage' // Styles import styles from './Styles/<%= props.name %>Style' class <%= props.name %> extends Component { state: { dataSource: Object } constructor (props) { super(props) /* *********************************************************** * STEP 1 * This is an array of objects with the properties you desire * Usually this should come from Redux mapStateToProps *************************************************************/ const dataObjects = [ {title: 'First Title', description: 'First Description'}, {title: 'Second Title', description: 'Second Description'}, {title: 'Third Title', description: 'Third Description'}, {title: 'Fourth Title', description: 'Fourth Description'}, {title: 'Fifth Title', description: 'Fifth Description'}, {title: 'Sixth Title', description: 'Sixth Description'}, {title: 'Seventh Title', description: 'Seventh Description'} ] /* *********************************************************** * STEP 2 * Teach datasource how to detect if rows are different * Make this function fast! Perhaps something like: * (r1, r2) => r1.id !== r2.id} *************************************************************/ const rowHasChanged = (r1, r2) => r1 !== r2 // DataSource configured const ds = new ListView.DataSource({rowHasChanged}) // Datasource is always in state this.state = { dataSource: ds.cloneWithRows(dataObjects) } } /* *********************************************************** * STEP 3 * `renderRow` function -How each cell/row should be rendered * It's our best practice to place a single component here: * * e.g. return *************************************************************/ renderRow (rowData) { return ( {rowData.title} {rowData.description} ) } /* *********************************************************** * STEP 4 * If your datasource is driven by Redux, you'll need to * reset it when new data arrives. * DO NOT! place `cloneWithRows` inside of render, since render * is called very often, and should remain fast! Just replace * state's datasource on newProps. * * e.g. componentWillReceiveProps (newProps) { if (newProps.someData) { this.setState(prevState => ({ dataSource: prevState.dataSource.cloneWithRows(newProps.someData) })) } } *************************************************************/ // Used for friendly AlertMessage // returns true if the dataSource is empty noRowData () { return this.state.dataSource.getRowCount() === 0 } // Render a footer. renderFooter = () => { return ( - Footer - ) } render () { return ( ) } } const mapStateToProps = (state) => { return { // ...redux state to props here } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: templates/redux-test-ava.ejs ================================================ import test from 'ava' import Actions, { reducer, INITIAL_STATE } from '../../App/Redux/<%= props.name %>Redux' test('attempt', t => { const state = reducer(INITIAL_STATE, Actions.<%= camelCase(props.name) %>Request('data')) t.true(state.fetching) }) test('success', t => { const state = reducer(INITIAL_STATE, Actions.<%= camelCase(props.name) %>Success('hi')) t.is(state.payload, 'hi') }) test('failure', t => { const state = reducer(INITIAL_STATE, Actions.<%= camelCase(props.name) %>Failure(99)) t.false(state.fetching) t.true(state.error) }) ================================================ FILE: templates/redux-test-jest.ejs ================================================ import Actions, { reducer, INITIAL_STATE } from '../../App/Redux/<%= props.name %>Redux' it('attempt', () => { const state = reducer(INITIAL_STATE, Actions.<%= camelCase(props.name) %>Request('data')) expect(state.fetching).toBe(true) }) it('success', () => { const state = reducer(INITIAL_STATE, Actions.<%= camelCase(props.name) %>Success('hi')) expect(state.payload).toBe('hi') }) it('failure', () => { const state = reducer(INITIAL_STATE, Actions.<%= camelCase(props.name) %>Failure()) expect(state.fetching).toBe(false) expect(state.error).toBe(true) }) ================================================ FILE: templates/redux.ejs ================================================ import { createReducer, createActions } from 'reduxsauce' import Immutable from 'seamless-immutable' /* ------------- Types and Action Creators ------------- */ const { Types, Creators } = createActions({ <%= camelCase(props.name) %>Request: ['data'], <%= camelCase(props.name) %>Success: ['payload'], <%= camelCase(props.name) %>Failure: null }) export const <%= props.name %>Types = Types export default Creators /* ------------- Initial State ------------- */ export const INITIAL_STATE = Immutable({ data: null, fetching: null, payload: null, error: null }) /* ------------- Selectors ------------- */ export const <%= props.name %>Selectors = { getData: state => state.data } /* ------------- Reducers ------------- */ // request the data from an api export const request = (state, { data }) => state.merge({ fetching: true, data, payload: null }) // successful api lookup export const success = (state, action) => { const { payload } = action return state.merge({ fetching: false, error: null, payload }) } // Something went wrong somewhere. export const failure = state => state.merge({ fetching: false, error: true, payload: null }) /* ------------- Hookup Reducers To Types ------------- */ export const reducer = createReducer(INITIAL_STATE, { [Types.<%= snakeCase(props.name).toUpperCase() %>_REQUEST]: request, [Types.<%= snakeCase(props.name).toUpperCase() %>_SUCCESS]: success, [Types.<%= snakeCase(props.name).toUpperCase() %>_FAILURE]: failure }) ================================================ FILE: templates/saga-test-ava.ejs ================================================ /* *********************************************************** * Wiring Instructions * To make this test work, you'll need to: * - Add a Fixture named get<%= props.name %> to the * ./App/Services/FixtureApi file. You can just keep adding * functions to that file. *************************************************************/ import test from 'ava' import FixtureAPI from '../../App/Services/FixtureApi' import { call, put } from 'redux-saga/effects' import { get<%= props.name %> } from '../../App/Sagas/<%= props.name %>Sagas' import <%= props.name %>Actions from '../../App/Redux/<%= props.name %>Redux' const stepper = (fn) => (mock) => fn.next(mock).value test('first calls API', t => { const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) // first yield is the API t.deepEqual(step(), call(FixtureAPI.get<%= props.name %>, 'taco')) }) test('success path', t => { const response = FixtureAPI.get<%= props.name %>('taco') const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) // Step 1: hit the api step() // Second step successful return and data! t.deepEqual(step(response), put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Success(21))) }) test('failure path', t => { const response = {ok: false} const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) // Step 1: hit the api step() // Second step failed response t.deepEqual(step(response), put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Failure())) }) ================================================ FILE: templates/saga-test-jest.ejs ================================================ /* *********************************************************** * Wiring Instructions * To make this test work, you'll need to: * - Add a Fixture named get<%= props.name %> to the * ./App/Services/FixtureApi file. You can just keep adding * functions to that file. *************************************************************/ import FixtureAPI from '../../App/Services/FixtureApi' import { call, put } from 'redux-saga/effects' import { get<%= props.name %> } from '../../App/Sagas/<%= props.name %>Sagas' import <%= props.name %>Actions from '../../App/Redux/<%= props.name %>Redux' const stepper = (fn) => (mock) => fn.next(mock).value it('first calls API', () => { const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) // first yield is the API expect(step()).toEqual(call(FixtureAPI.get<%= props.name %>, 'taco')) }) it('success path', () => { const response = FixtureAPI.get<%= props.name %>('taco') const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) // Step 1: Hit the api step() // Step 2: Successful return and data! expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Success(21))) }) it('failure path', () => { const response = {ok: false} const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) // Step 1: Hit the api step() // Step 2: Failed response. expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Failure())) }) ================================================ FILE: templates/saga.ejs ================================================ /* *********************************************************** * A short word on how to use this automagically generated file. * We're often asked in the Infinite Red Slack channel how to connect * to a to a third party api, so we thought we'd demonstrate - but * you should know you can use sagas for other flow control too. * * Other points: * - You'll need to add this saga to sagas/index.js * - This template uses the api declared in sagas/index.js, so * you'll need to define a constant in that file. *************************************************************/ import { call, put } from 'redux-saga/effects' import <%= props.name %>Actions from '../Redux/<%= props.name %>Redux' // import { <%= props.name %>Selectors } from '../Redux/<%= props.name %>Redux' export function * get<%= props.name %> (api, action) { const { data } = action // get current data from Store // const currentData = yield select(<%= props.name %>Selectors.getData) // make the call to the api const response = yield call(api.get<%= camelCase(props.name) %>, data) // success? if (response.ok) { // You might need to change the response here - do this with a 'transform', // located in ../Transforms/. Otherwise, just pass the data back from the api. yield put(<%= props.name %>Actions.<%= camelCase(props.name) %>Success(response.data)) } else { yield put(<%= props.name %>Actions.<%= camelCase(props.name) %>Failure()) } } ================================================ FILE: templates/screen-style.ejs ================================================ import { StyleSheet } from 'react-native' import { ApplicationStyles } from '../../Themes/' export default StyleSheet.create({ ...ApplicationStyles.screen }) ================================================ FILE: templates/screen.ejs ================================================ import React, { Component } from 'react' import { ScrollView, Text, KeyboardAvoidingView } from 'react-native' import { connect } from 'react-redux' // Add Actions - replace 'Your' with whatever your reducer is called :) // import YourActions from '../Redux/YourRedux' // Styles import styles from './Styles/<%= props.name %>Style' class <%= props.name %> extends Component { render () { return ( <%= props.name %> ) } } const mapStateToProps = (state) => { return { } } const mapDispatchToProps = (dispatch) => { return { } } export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) ================================================ FILE: test/generators-integration.test.js ================================================ const execa = require('execa') const jetpack = require('fs-jetpack') const tempy = require('tempy') const IGNITE = 'npx ignite-cli' const APP = 'IntegrationTest' const BOILERPLATE = jetpack.path(__dirname, '..') // calling the ignite cli takes a while jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000 const exopts = { preferLocal: false, shell: true } describe('generators', () => { beforeAll(async () => { // creates a new temp directory process.chdir(tempy.directory()) await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--boilerplate', BOILERPLATE], exopts) process.chdir(APP) await execa('npm', ['run', 'fixcode'], exopts) }) test('generates a component', async () => { const simpleComponent = 'Simple' await execa(IGNITE, ['g', 'component', simpleComponent], exopts) expect(jetpack.exists(`App/Components/${simpleComponent}.js`)).toBe('file') expect(jetpack.exists(`App/Components/Styles/${simpleComponent}Style.js`)).toBe('file') const lint = await execa('npm', ['-s', 'run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generates a folder component', async () => { const folderComponent = 'Folder' await execa(IGNITE, ['g', 'component', '--folder', folderComponent], exopts) expect(jetpack.exists(`App/Components/${folderComponent}/index.js`)).toBe('file') expect(jetpack.exists(`App/Components/${folderComponent}/Styles/indexStyle.js`)).toBe('file') const lint = await execa('npm', ['-s', 'run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generates a component inside a folder', async () => { const componentName = 'InFolder' const folderName = 'Folder' await execa(IGNITE, ['g', 'component', '--folder', folderName, componentName], exopts) expect(jetpack.exists(`App/Components/${folderName}/${componentName}.js`)).toBe('file') expect(jetpack.exists(`App/Components/${folderName}/Styles/${componentName}Style.js`)).toBe('file') const lint = await execa('npm', ['-s', 'run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generates a component in a relative path', async () => { await execa(IGNITE, ['g', 'component', 'My/SubFolder/Test'], exopts) expect(jetpack.exists('App/Components/My/SubFolder/Test.js')).toBe('file') expect(jetpack.exists('App/Components/My/SubFolder/Styles/TestStyle.js')).toBe('file') const lint = await execa('npm', ['-s', 'run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate listview of type row works', async () => { await execa(IGNITE, ['g', 'list', 'TestRow', '--type=Row', '--codeType=listview', '--dataType=Single'], exopts) expect(jetpack.exists('App/Containers/TestRow.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/TestRowStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate flatlist of type row works', async () => { await execa(IGNITE, ['g', 'list', 'TestFlatRow', '--type=Row', '--codeType=flatlist', '--dataType=Single'], exopts) expect(jetpack.exists('App/Containers/TestFlatRow.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/TestFlatRowStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate listview of sections works', async () => { await execa( IGNITE, ['g', 'list', 'TestSection', '--type=Row', '--codeType=listview', '--dataType=Sectioned'], exopts ) expect(jetpack.exists('App/Containers/TestSection.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/TestSectionStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate flatlist of sections works', async () => { await execa( IGNITE, ['g', 'list', 'TestFlatSection', '--type=Row', '--codeType=flatlist', '--dataType=Sectioned'], exopts ) expect(jetpack.exists('App/Containers/TestFlatSection.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/TestFlatSectionStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate listview of type grid works', async () => { await execa(IGNITE, ['g', 'list', 'TestGrid', '--type=Grid', '--codeType=listview', '--dataType=Single'], exopts) expect(jetpack.exists('App/Containers/TestGrid.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/TestGridStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate redux works', async () => { await execa(IGNITE, ['g', 'redux', 'Test'], exopts) expect(jetpack.exists('App/Redux/TestRedux.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate container works', async () => { await execa(IGNITE, ['g', 'container', 'Container'], exopts) expect(jetpack.exists('App/Containers/Container.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/ContainerStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate saga works', async () => { await execa(IGNITE, ['g', 'saga', 'Test'], exopts) expect(jetpack.exists('App/Sagas/TestSagas.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) test('generate screen works', async () => { await execa(IGNITE, ['g', 'screen', 'Test'], exopts) expect(jetpack.exists('App/Containers/TestScreen.js')).toBe('file') expect(jetpack.exists('App/Containers/Styles/TestScreenStyle.js')).toBe('file') const lint = await execa('npm', ['run', 'lint', '--loglevel=error'], exopts) expect(lint.stderr).toBe('') }) }) ================================================ FILE: test/interface.test.js ================================================ const boilerplate = require('../boilerplate') const plugin = require('../plugin') test('boilerplate interface', async () => { expect(typeof boilerplate.install).toBe('function') }) test('plugin interface', async () => { expect(typeof plugin.add).toBe('function') expect(typeof plugin.remove).toBe('function') }) ================================================ FILE: test/react-native-version.test.js ================================================ const boilerplate = require('../lib/react-native-version') // grab a few things from the boilerplate module const get = boilerplate.getReactNativeVersion const DEFAULT = boilerplate.REACT_NATIVE_VERSION /** * Runs with a valid gluegun context and a staged version number. * * @param {*} reactNativeVersion The React Native version to use. * @return {string} The version number we should be using. */ const mock = reactNativeVersion => get({ parameters: { options: { 'react-native-version': reactNativeVersion } } }) // this would only happen if we screwed something up in our boilerplate.js test('it handles strange inputs from code', () => { expect(get()).toBe(DEFAULT) expect(get(null)).toBe(DEFAULT) expect(get(true)).toBe(DEFAULT) expect(get(8)).toBe(DEFAULT) expect(get('hello')).toBe(DEFAULT) expect(get([])).toBe(DEFAULT) expect(get({})).toBe(DEFAULT) expect(get(() => true)).toBe(DEFAULT) }) // this could happen because it's valid input via minimist from the user test('it handles strange input from the user', () => { expect(mock(true)).toBe(DEFAULT) expect(mock(false)).toBe(DEFAULT) expect(mock([])).toBe(DEFAULT) expect(mock({})).toBe(DEFAULT) }) // very edge-casey test('it handles not-quite semver numbers', () => { expect(mock(0)).toBe(DEFAULT) expect(mock(0.25)).toBe(DEFAULT) }) // happy path test('it handles valid versions', () => { expect(mock('0.41.0')).toBe('0.41.0') expect(mock('0.41.0-beta.1')).toBe('0.41.0-beta.1') expect(mock(DEFAULT)).toBe(DEFAULT) expect(mock('next')).toBe('next') })