├── boilerplate ├── App │ ├── Images │ │ ├── README.md │ │ ├── BG.png │ │ ├── ir.png │ │ ├── tile_bg.png │ │ ├── top_logo.png │ │ ├── your-app.png │ │ ├── button-bg@2x.png │ │ ├── ignite_logo.png │ │ ├── launch-icon.png │ │ ├── top_logo@2x.png │ │ ├── top_logo@3x.png │ │ ├── your-app@2x.png │ │ ├── your-app@3x.png │ │ ├── Icons │ │ │ ├── faq-icon.png │ │ │ ├── faq-icon@2x.png │ │ │ ├── faq-icon@3x.png │ │ │ ├── close-button.png │ │ │ ├── hamburger@2x.png │ │ │ ├── icon-home@2x.png │ │ │ ├── icon-theme@2x.png │ │ │ ├── back-button@2x.png │ │ │ ├── chevron-right@2x.png │ │ │ ├── close-button@2x.png │ │ │ ├── close-button@3x.png │ │ │ ├── icon-components@2x.png │ │ │ ├── icon-api-testing@2x.png │ │ │ ├── icon-usage-examples@2x.png │ │ │ └── icon-device-information@2x.png │ │ ├── launch-icon@2x.png │ │ ├── launch-icon@3x.png │ │ └── ignite-logo-transparent.png │ ├── Components │ │ ├── FullButton │ │ │ ├── index.ts │ │ │ ├── FullButton.story.tsx │ │ │ ├── FullButtonStyles.ts │ │ │ ├── FullButtonTest.tsx │ │ │ └── FullButton.tsx │ │ ├── AlertMessage │ │ │ ├── index.ts │ │ │ ├── AlertMessage.story.tsx │ │ │ ├── AlertMessage.tsx │ │ │ ├── AlertMessageStyles.ts │ │ │ └── AlertMessageTest.tsx │ │ ├── DrawerButton │ │ │ ├── index.ts │ │ │ ├── DrawerButtonStyles.ts │ │ │ ├── DrawerButton.story.tsx │ │ │ ├── DrawerButtonTest.tsx │ │ │ └── DrawerButton.tsx │ │ ├── RoundedButton │ │ │ ├── index.ts │ │ │ ├── RoundedButton.story.tsx │ │ │ ├── RoundedButtonStyles.ts │ │ │ ├── RoundedButtonTest.tsx │ │ │ └── RoundedButton.tsx │ │ ├── Stories.tsx │ │ └── README.md │ ├── Containers │ │ ├── LaunchScreen │ │ │ ├── index.ts │ │ │ ├── LaunchScreenStyles.ts │ │ │ └── LaunchScreen.tsx │ │ ├── RootContainer │ │ │ ├── index.ts │ │ │ ├── RootContainerStyles.ts │ │ │ └── RootContainer.tsx │ │ ├── README.md │ │ └── App.tsx │ ├── Themes │ │ ├── README.md │ │ ├── index.ts │ │ ├── Colors.ts │ │ ├── Metrics.ts │ │ ├── Fonts.ts │ │ ├── Images.ts │ │ └── ApplicationStyles.ts │ ├── Services │ │ ├── Api.ts │ │ ├── ImmutablePersistenceTransform.tsx │ │ ├── FixtureApi.tsx │ │ ├── RehydrationServices.tsx │ │ ├── FixtureAPITest.tsx │ │ ├── ExamplesRegistry.tsx │ │ └── GithubApi.tsx │ ├── Config │ │ ├── AppConfig.ts │ │ ├── README.md │ │ ├── DebugConfig.js │ │ ├── index.ts │ │ ├── ReduxPersist.ts │ │ └── ReactotronConfig.ts │ ├── Transforms │ │ ├── ConvertFromKelvin.ts │ │ └── README.md │ ├── Navigation │ │ ├── Styles │ │ │ └── NavigationStyles.ts │ │ ├── AppNavigation.tsx │ │ └── ReduxNavigation.tsx │ ├── Reducers │ │ ├── StartupReducers │ │ │ └── index.tsx │ │ ├── NavigationReducers │ │ │ └── index.tsx │ │ ├── GithubReducers │ │ │ ├── GithubReducersTest.tsx │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── ScreenTrackingMiddleware.tsx │ │ └── CreateStore.tsx │ ├── Fixtures │ │ ├── README.md │ │ ├── rateLimit.json │ │ ├── skellock.json │ │ ├── root.json │ │ └── gantman.json │ ├── Lib │ │ ├── README.md │ │ └── ReduxHelpers.ts │ └── Sagas │ │ ├── StartupSagas │ │ ├── StartupSagaTest.ts │ │ └── index.ts │ │ ├── GithubSagas │ │ ├── index.ts │ │ └── GithubSagaTest.ts │ │ └── index.ts ├── storybook │ ├── index.js │ ├── addons.js │ └── storybook.ejs ├── Tests │ ├── StoriesTest.ts │ └── Setup.tsx.ejs ├── .babelrc ├── ignite.json.ejs ├── index.js.ejs ├── rn-cli.config.js ├── .env.example ├── tslint.json ├── .editorconfig ├── types │ ├── @storybook │ │ └── react-native.d.ts │ └── reduxsauce │ │ └── index.d.ts ├── README.md ├── package.json.ejs └── tsconfig.json ├── templates ├── screen-index.ejs ├── component-index.ejs ├── component-style.ejs ├── component-story.ejs ├── container-style.ejs ├── screen-style.ejs ├── component-test-jest.ejs ├── component.ejs ├── reducers-test-jest.ejs ├── flatlist-grid-style.ejs ├── listview-grid-style.ejs ├── container.ejs ├── saga-test-jest.ejs ├── saga.ejs ├── screen.ejs ├── reducers.ejs ├── flatlist.ejs ├── flatlist-grid.ejs └── flatlist-sections.ejs ├── ignite.json ├── lib ├── patterns.js └── react-native-version.js ├── .gitignore ├── test ├── interface.test.js ├── react-native-version.test.js └── generators-integration.test.js ├── plugin.js ├── commands ├── saga.js ├── reducers.js ├── component.js ├── container.js ├── screen.js └── list.js ├── options.js ├── package.json ├── tsconfig.json ├── readme.md └── boilerplate.js /boilerplate/App/Images/README.md: -------------------------------------------------------------------------------- 1 | ### Images folder 2 | Holds all images for the applications. -------------------------------------------------------------------------------- /boilerplate/storybook/index.js: -------------------------------------------------------------------------------- 1 | import StorybookUI from './storybook' 2 | 3 | export default StorybookUI 4 | -------------------------------------------------------------------------------- /boilerplate/Tests/StoriesTest.ts: -------------------------------------------------------------------------------- 1 | import initStoryshots from "@storybook/addon-storyshots"; 2 | 3 | initStoryshots(); 4 | -------------------------------------------------------------------------------- /boilerplate/storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register' 2 | import '@storybook/addon-links/register' 3 | -------------------------------------------------------------------------------- /boilerplate/App/Components/FullButton/index.ts: -------------------------------------------------------------------------------- 1 | import FullButton from "./FullButton"; 2 | 3 | export default FullButton; 4 | -------------------------------------------------------------------------------- /templates/screen-index.ejs: -------------------------------------------------------------------------------- 1 | import <%= props.name %> from "./<%= props.name %>"; 2 | 3 | export default <%= props.name %>; 4 | -------------------------------------------------------------------------------- /boilerplate/App/Components/AlertMessage/index.ts: -------------------------------------------------------------------------------- 1 | import AlertMessage from "./AlertMessage"; 2 | 3 | export default AlertMessage; 4 | -------------------------------------------------------------------------------- /boilerplate/App/Components/DrawerButton/index.ts: -------------------------------------------------------------------------------- 1 | import DrawerButton from "./DrawerButton"; 2 | 3 | export default DrawerButton; 4 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/LaunchScreen/index.ts: -------------------------------------------------------------------------------- 1 | import LaunchScreen from "./LaunchScreen"; 2 | 3 | export default LaunchScreen; 4 | -------------------------------------------------------------------------------- /templates/component-index.ejs: -------------------------------------------------------------------------------- 1 | import <%= props.name %> from "./<%= props.name %>"; 2 | 3 | export default <%= props.name %>; 4 | -------------------------------------------------------------------------------- /boilerplate/App/Components/RoundedButton/index.ts: -------------------------------------------------------------------------------- 1 | import RoundedButton from "./RoundedButton"; 2 | 3 | export default RoundedButton; 4 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/RootContainer/index.ts: -------------------------------------------------------------------------------- 1 | import RootContainer from "./RootContainer"; 2 | 3 | export default RootContainer; 4 | -------------------------------------------------------------------------------- /boilerplate/App/Images/BG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/BG.png -------------------------------------------------------------------------------- /boilerplate/App/Images/ir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/ir.png -------------------------------------------------------------------------------- /boilerplate/App/Images/tile_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/tile_bg.png -------------------------------------------------------------------------------- /boilerplate/App/Images/top_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/top_logo.png -------------------------------------------------------------------------------- /boilerplate/App/Images/your-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/your-app.png -------------------------------------------------------------------------------- /boilerplate/App/Images/button-bg@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/button-bg@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/ignite_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/ignite_logo.png -------------------------------------------------------------------------------- /boilerplate/App/Images/launch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/launch-icon.png -------------------------------------------------------------------------------- /boilerplate/App/Images/top_logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/top_logo@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/top_logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/top_logo@3x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/your-app@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/your-app@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/your-app@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/your-app@3x.png -------------------------------------------------------------------------------- /boilerplate/App/Themes/README.md: -------------------------------------------------------------------------------- 1 | ### Themes Folder 2 | Application specific themes 3 | * Base Styles 4 | * Fonts 5 | * Metrics 6 | * Colors 7 | 8 | etc. 9 | -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/faq-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/faq-icon.png -------------------------------------------------------------------------------- /boilerplate/App/Images/launch-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/launch-icon@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/launch-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/launch-icon@3x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/faq-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/faq-icon@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/faq-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/faq-icon@3x.png -------------------------------------------------------------------------------- /boilerplate/App/Services/Api.ts: -------------------------------------------------------------------------------- 1 | // This is for the devscreens 2 | import {createAPI} from "./GithubApi"; 3 | 4 | export default { 5 | create: createAPI, 6 | }; 7 | -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/close-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/close-button.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/hamburger@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/hamburger@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/icon-home@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/icon-home@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/icon-theme@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/icon-theme@2x.png -------------------------------------------------------------------------------- /templates/component-style.ejs: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export default StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /boilerplate/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"], 3 | "env": { 4 | "production": { 5 | "plugins": ["ignite-ignore-reactotron"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/back-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/back-button@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/chevron-right@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/chevron-right@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/close-button@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/close-button@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/close-button@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/close-button@3x.png -------------------------------------------------------------------------------- /ignite.json: -------------------------------------------------------------------------------- 1 | { 2 | "generators": [ 3 | "component", 4 | "container", 5 | "list", 6 | "reducers", 7 | "saga", 8 | "screen" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/icon-components@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/icon-components@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Images/ignite-logo-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/ignite-logo-transparent.png -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/icon-api-testing@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/icon-api-testing@2x.png -------------------------------------------------------------------------------- /boilerplate/ignite.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "createdWith": "<%= props.igniteVersion %>", 3 | "examples": "classic", 4 | "navigation": "react-navigation", 5 | "askToOverride": true 6 | } 7 | -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/icon-usage-examples@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/icon-usage-examples@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Config/AppConfig.ts: -------------------------------------------------------------------------------- 1 | // Simple React Native specific changes 2 | 3 | export default { 4 | // font scaling override - RN default is on 5 | allowTextFontScaling: true, 6 | }; 7 | -------------------------------------------------------------------------------- /boilerplate/App/Images/Icons/icon-device-information@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aerian-studios/ignite-typescript-boilerplate/HEAD/boilerplate/App/Images/Icons/icon-device-information@2x.png -------------------------------------------------------------------------------- /boilerplate/App/Components/Stories.tsx: -------------------------------------------------------------------------------- 1 | import "./AlertMessage/AlertMessage.story"; 2 | import "./DrawerButton/DrawerButton.story"; 3 | import "./FullButton/FullButton.story"; 4 | import "./RoundedButton/RoundedButton.story"; 5 | -------------------------------------------------------------------------------- /boilerplate/App/Transforms/ConvertFromKelvin.ts: -------------------------------------------------------------------------------- 1 | export default (kelvin: number) => { 2 | const celsius = kelvin - 273.15; 3 | const fahrenheit = (celsius * 1.8000) + 32; 4 | 5 | return Math.round(fahrenheit); 6 | }; 7 | -------------------------------------------------------------------------------- /boilerplate/index.js.ejs: -------------------------------------------------------------------------------- 1 | import "./App/Config/ReactotronConfig" 2 | import { AppRegistry } from 'react-native'; 3 | import App from "./App/Containers/App"; 4 | 5 | AppRegistry.registerComponent("<%= props.name %>", () => App); 6 | -------------------------------------------------------------------------------- /boilerplate/rn-cli.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getTransformModulePath() { 3 | return require.resolve('react-native-typescript-transformer') 4 | }, 5 | getSourceExts() { 6 | return ['ts', 'tsx']; 7 | } 8 | } -------------------------------------------------------------------------------- /boilerplate/App/Navigation/Styles/NavigationStyles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { Colors } from "../../Themes/index"; 3 | 4 | export default StyleSheet.create({ 5 | header: { 6 | backgroundColor: Colors.background, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /templates/component-story.ejs: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react-native"; 2 | import * as React from "react"; 3 | 4 | import <%= props.name %> from "./<%= props.name %>"; 5 | 6 | storiesOf("<%= props.name %>", module) 7 | .add("Default", () => ( 8 | <<%= props.name %> /> 9 | )); -------------------------------------------------------------------------------- /boilerplate/App/Themes/index.ts: -------------------------------------------------------------------------------- 1 | import ApplicationStyles from "./ApplicationStyles"; 2 | import Colors from "./Colors"; 3 | import Fonts from "./Fonts"; 4 | import Images from "./Images"; 5 | import Metrics from "./Metrics"; 6 | 7 | export { Colors, Fonts, Images, Metrics, ApplicationStyles }; 8 | -------------------------------------------------------------------------------- /boilerplate/App/Reducers/StartupReducers/index.tsx: -------------------------------------------------------------------------------- 1 | import { createAction } from "typesafe-actions"; 2 | 3 | /* ------------- Types and Action Creators ------------- */ 4 | 5 | const actions = { 6 | startup: createAction("startup"), 7 | }; 8 | 9 | export const StartupActions = actions; 10 | -------------------------------------------------------------------------------- /lib/patterns.js: -------------------------------------------------------------------------------- 1 | const constants = { 2 | PATTERN_IMPORTS: 'imports', 3 | PATTERN_ROUTES: 'routes' 4 | } 5 | 6 | module.exports = { 7 | constants, 8 | [constants.PATTERN_IMPORTS]: `import[\\s\\S]*from\\s+"react-navigation";?`, 9 | [constants.PATTERN_ROUTES]: 'const PrimaryNav' 10 | } 11 | -------------------------------------------------------------------------------- /boilerplate/App/Config/README.md: -------------------------------------------------------------------------------- 1 | ### Config Folder 2 | All application specific configuration falls in this folder. 3 | 4 | `AppConfig.js` - production values. 5 | `DebugConfig.js` - development-wide globals. 6 | `ReactotronConfig.js` - Reactotron client settings. 7 | `ReduxPersist.js` - rehydrate Redux state. 8 | -------------------------------------------------------------------------------- /templates/container-style.ejs: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { Colors, Metrics } from "../../Themes/"; 3 | 4 | export default StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | marginTop: Metrics.navBarHeight, 8 | backgroundColor: Colors.background, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /templates/screen-style.ejs: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { Colors, Metrics } from "../../Themes/"; 3 | 4 | export default StyleSheet.create({ 5 | container: { 6 | flex: 1, 7 | marginTop: Metrics.navBarHeight, 8 | backgroundColor: Colors.background, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /boilerplate/.env.example: -------------------------------------------------------------------------------- 1 | # This is an example where you can store your environment variables. Copy this file to ./App/Config/.env 2 | # Now your app will have access to the variables added below. 3 | # For more instructions see section "Secrets" in README.md 4 | 5 | API_URL=https://myapi.com 6 | GOOGLE_MAPS_API_KEY=abcdefgh -------------------------------------------------------------------------------- /boilerplate/App/Fixtures/README.md: -------------------------------------------------------------------------------- 1 | ### Fixtures folder 2 | All key API responses are housed here. 3 | 4 | These API responses can be used for several reasons. _E.G._: 5 | * To bypass logins when building any screen of the application 6 | * To quickly test API parsing in unit tests 7 | * To separate Network from Data concerns while coding 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | npm-debug.log 5 | npm-debug.log* 6 | coverage 7 | .nyc_output 8 | yarn.lock 9 | lerna-debug.log 10 | node_modules 11 | .vscode/* 12 | !.vscode/settings.json 13 | !.vscode/tasks.json 14 | !.vscode/launch.json 15 | !.vscode/extensions.json 16 | testgrounds 17 | IntegrationTest 18 | integration_test 19 | -------------------------------------------------------------------------------- /boilerplate/App/Config/DebugConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a .js file because the Ignite scripts need to find it 3 | */ 4 | export default { 5 | showDevScreens: __DEV__, 6 | useFixtures: false, 7 | ezLogin: false, 8 | yellowBox: __DEV__, 9 | reduxLogging: __DEV__, 10 | includeExamples: __DEV__, 11 | useReactotron: __DEV__, 12 | }; 13 | -------------------------------------------------------------------------------- /boilerplate/App/Components/DrawerButton/DrawerButtonStyles.ts: -------------------------------------------------------------------------------- 1 | import Colors from "../../Themes/Colors"; 2 | import Fonts from "../../Themes/Fonts"; 3 | import Metrics from "../../Themes/Metrics"; 4 | 5 | export default { 6 | text: { 7 | ...Fonts.style.h5, 8 | color: Colors.snow, 9 | marginVertical: Metrics.baseMargin, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /boilerplate/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", "tslint-react" 4 | ], 5 | "rules": { 6 | "interface-name": false, 7 | "object-literal-sort-keys": false, 8 | "no-object-literal-type-assertion": false, 9 | "no-empty-interface": false, 10 | "no-submodule-imports": false 11 | } 12 | } -------------------------------------------------------------------------------- /test/interface.test.js: -------------------------------------------------------------------------------- 1 | const boilerplate = require('../boilerplate') 2 | const plugin = require('../plugin') 3 | 4 | test('boilerplate interface', async () => { 5 | expect(typeof boilerplate.install).toBe('function') 6 | }) 7 | 8 | test('plugin interface', async () => { 9 | expect(typeof plugin.add).toBe('function') 10 | expect(typeof plugin.remove).toBe('function') 11 | }) 12 | -------------------------------------------------------------------------------- /boilerplate/App/Components/README.md: -------------------------------------------------------------------------------- 1 | ### Components Folder 2 | All components are stored and organized here 3 | 4 | "In an ideal world, most of your components would be stateless functions because in the future we’ll also be able to make performance optimizations specific to these components by avoiding unnecessary checks and memory allocations. This is the recommended pattern, when possible." --React docs -------------------------------------------------------------------------------- /boilerplate/App/Fixtures/rateLimit.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": { 3 | "core": { 4 | "limit": 60, 5 | "remaining": 42, 6 | "reset": 1488126913 7 | }, 8 | "search": { 9 | "limit": 10, 10 | "remaining": 9, 11 | "reset": 1488126003 12 | } 13 | }, 14 | "rate": { 15 | "limit": 60, 16 | "remaining": 42, 17 | "reset": 1488126913 18 | } 19 | } -------------------------------------------------------------------------------- /boilerplate/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | 15 | 16 | [*.gradle] 17 | indent_size = 4 -------------------------------------------------------------------------------- /templates/component-test-jest.ejs: -------------------------------------------------------------------------------- 1 | /// 2 | import * as React from "react"; 3 | import { <%= props.name %> } from "./<%= props.name %>"; 4 | import * as renderer from "react-test-renderer"; 5 | 6 | 7 | test("<%= props.name %> component renders correctly", () => { 8 | const tree = renderer.create(<<%= props.name %> someProperty="howdy" anotherProperty={false} />).toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); -------------------------------------------------------------------------------- /boilerplate/App/Reducers/NavigationReducers/index.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationAction, NavigationState } from "react-navigation"; 2 | import AppNavigation from "../../Navigation/AppNavigation"; 3 | 4 | export type NavigationState = NavigationState; 5 | 6 | export const NavigationReducer = (state: NavigationState, action: NavigationAction) => { 7 | const newState = AppNavigation.router.getStateForAction(action, state); 8 | return newState || state; 9 | }; 10 | -------------------------------------------------------------------------------- /boilerplate/App/Components/FullButton/FullButton.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react-native"; 2 | import * as React from "react"; 3 | 4 | import FullButton from "./FullButton"; 5 | 6 | storiesOf("FullButton") 7 | .add("Default", () => ( 8 | 11 | )) 12 | .add("Custom Style", () => ( 13 | 17 | )); 18 | -------------------------------------------------------------------------------- /boilerplate/App/Lib/README.md: -------------------------------------------------------------------------------- 1 | # Lib 2 | 3 | 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. 4 | 5 | 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. 6 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | // Ignite CLI plugin for Ts 2 | // ---------------------------------------------------------------------------- 3 | 4 | 5 | const add = async function (context) { 6 | // No-op, as we do this all in `boilerplate.js` 7 | } 8 | 9 | /** 10 | * Remove yourself from the project. 11 | */ 12 | const remove = async function (context) { 13 | // No-op, as we do this all in `boilerplate.js` 14 | } 15 | 16 | // Required in all Ignite CLI plugins 17 | module.exports = { add, remove } 18 | -------------------------------------------------------------------------------- /templates/component.ejs: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, View } from "react-native"; 3 | import styles from "./<%= props.name %>Style"; 4 | 5 | interface Props { 6 | someProperty: string; 7 | anotherProperty: boolean; 8 | } 9 | 10 | const <%= props.name %>: React.SFC = ({someProperty, anotherProperty}: Props) => ( 11 | 12 | <%= props.name %> Component 13 | 14 | ); 15 | 16 | export default <%= props.name %>; 17 | -------------------------------------------------------------------------------- /boilerplate/App/Components/RoundedButton/RoundedButton.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react-native"; 2 | import * as React from "react"; 3 | 4 | import RoundedButton from "./RoundedButton"; 5 | 6 | storiesOf("RoundedButton", module) 7 | .add("Default", () => ( 8 | 11 | )) 12 | .add("Text as children", () => ( 13 | 14 | Hello from the children! 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /boilerplate/App/Components/DrawerButton/DrawerButton.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react-native"; 2 | import * as React from "react"; 3 | import { View } from "react-native"; 4 | 5 | import DrawerButton from "./DrawerButton"; 6 | 7 | storiesOf("DrawerButton") 8 | .add("Default", () => ( 9 | 10 | alert("Hi")} 14 | /> 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/LaunchScreen/LaunchScreenStyles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { ApplicationStyles, Metrics } from "../../Themes/index"; 3 | 4 | export default StyleSheet.create({ 5 | ...ApplicationStyles.screen, 6 | container: { 7 | paddingBottom: Metrics.baseMargin, 8 | }, 9 | logo: { 10 | marginTop: Metrics.doubleSection, 11 | height: Metrics.images.logo, 12 | width: Metrics.images.logo, 13 | resizeMode: "contain", 14 | }, 15 | centered: { 16 | alignItems: "center", 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /boilerplate/App/Sagas/StartupSagas/StartupSagaTest.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { put, select } from "redux-saga/effects"; 3 | import { GithubActions } from "../../Reducers/GithubReducers"; 4 | import { selectAvatar, startup } from "./index"; 5 | 6 | const stepper = (fn) => (mock) => fn.next(mock).value; 7 | 8 | test("watches for the right action", () => { 9 | const step = stepper(startup()); 10 | expect(step()).toEqual(select(selectAvatar)); 11 | expect(step()).toEqual(put(GithubActions.userRequest({username: "ascorbic"}))); 12 | }); 13 | -------------------------------------------------------------------------------- /boilerplate/App/Navigation/AppNavigation.tsx: -------------------------------------------------------------------------------- 1 | import { StackNavigator } from "react-navigation"; 2 | import LaunchScreen from "../Containers/LaunchScreen"; 3 | 4 | import styles from "./Styles/NavigationStyles"; 5 | 6 | // Manifest of possible screens 7 | const PrimaryNav = StackNavigator({ 8 | LaunchScreen: { screen: LaunchScreen }, 9 | }, { 10 | // Default config for all screens 11 | headerMode: "none", 12 | initialRouteName: "LaunchScreen", 13 | navigationOptions: { 14 | headerStyle: styles.header, 15 | }, 16 | }); 17 | 18 | export default PrimaryNav; 19 | -------------------------------------------------------------------------------- /boilerplate/App/Config/index.ts: -------------------------------------------------------------------------------- 1 | import { Text } from "react-native"; 2 | import AppConfig from "./AppConfig"; 3 | import DebugConfig from "./DebugConfig"; 4 | 5 | // Allow/disallow font-scaling in app 6 | if (!Text.defaultProps) { 7 | Text.defaultProps = {}; 8 | } 9 | Text.defaultProps.allowFontScaling = AppConfig.allowTextFontScaling; 10 | 11 | if (__DEV__) { 12 | // If ReactNative's yellow box warnings are too much, it is possible to turn 13 | // it off, but the healthier approach is to fix the warnings. =) 14 | console.disableYellowBox = !DebugConfig.yellowBox; 15 | } 16 | -------------------------------------------------------------------------------- /boilerplate/storybook/storybook.ejs: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native' 2 | import { getStorybookUI, configure } from '@storybook/react-native' 3 | 4 | // import stories 5 | configure(() => { 6 | require('../App/Components/Stories') 7 | }, module) 8 | 9 | // This assumes that storybook is running on the same host as your RN packager, 10 | // to set manually use, e.g. host: 'localhost' option 11 | const StorybookUI = getStorybookUI({ port: 7007, onDeviceUI: true }) 12 | AppRegistry.registerComponent('<%= props.name %>', () => StorybookUI) 13 | export default StorybookUI 14 | -------------------------------------------------------------------------------- /boilerplate/App/Components/AlertMessage/AlertMessage.story.tsx: -------------------------------------------------------------------------------- 1 | import { storiesOf } from "@storybook/react-native"; 2 | import * as React from "react"; 3 | 4 | import AlertMessage from "./AlertMessage"; 5 | 6 | storiesOf("AlertMessage") 7 | .add("Default", () => ( 8 | 11 | )) 12 | .add("Hidden", () => ( 13 | 17 | )) 18 | .add("Custom Style", () => ( 19 | 23 | )); 24 | -------------------------------------------------------------------------------- /boilerplate/App/Transforms/README.md: -------------------------------------------------------------------------------- 1 | # Transforms 2 | 3 | A common pattern when working with APIs is to change data to play nice between your app & the API. 4 | 5 | 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. 6 | 7 | Transforms are not necessarily a bad thing (although an API might have you transforming more than you'd like). 8 | 9 | For example, you may: 10 | 11 | * turn appropriate strings to date objects 12 | * convert snake case to camel case 13 | * normalize or denormalize things 14 | * create lookup tables 15 | -------------------------------------------------------------------------------- /boilerplate/App/Components/FullButton/FullButtonStyles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import Colors from "../../Themes/Colors"; 3 | import Fonts from "../../Themes/Fonts"; 4 | 5 | export default StyleSheet.create({ 6 | button: { 7 | marginVertical: 5, 8 | borderTopColor: Colors.fire, 9 | borderBottomColor: Colors.bloodOrange, 10 | borderTopWidth: 1, 11 | borderBottomWidth: 1, 12 | backgroundColor: Colors.ember, 13 | }, 14 | buttonText: { 15 | margin: 18, 16 | textAlign: "center", 17 | color: Colors.snow, 18 | fontSize: Fonts.size.medium, 19 | fontFamily: Fonts.type.bold, 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /boilerplate/App/Navigation/ReduxNavigation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactNavigation from "react-navigation"; 3 | import { connect } from "react-redux"; 4 | import AppNavigation from "./AppNavigation"; 5 | 6 | // here is our redux-aware smart component 7 | function ReduxNavigation(props) { 8 | const { dispatch, nav } = props; 9 | const navigation = ReactNavigation.addNavigationHelpers({ 10 | dispatch, 11 | state: nav, 12 | }); 13 | 14 | return ; 15 | } 16 | 17 | const mapStateToProps = (state) => ({ nav: state.nav }); 18 | export default connect(mapStateToProps)(ReduxNavigation); 19 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/RootContainer/RootContainerStyles.ts: -------------------------------------------------------------------------------- 1 | import {StyleSheet, TextStyle, ViewStyle } from "react-native"; 2 | import {Colors, Fonts, Metrics} from "../../Themes/index"; 3 | 4 | export default StyleSheet.create({ 5 | applicationView: { 6 | flex: 1, 7 | }, 8 | container: { 9 | flex: 1, 10 | justifyContent: "center", 11 | backgroundColor: Colors.background, 12 | }, 13 | welcome: { 14 | fontSize: 20, 15 | textAlign: "center", 16 | fontFamily: Fonts.type.base, 17 | margin: Metrics.baseMargin, 18 | }, 19 | myImage: { 20 | width: 200, 21 | height: 200, 22 | alignSelf: "center", 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /templates/reducers-test-jest.ejs: -------------------------------------------------------------------------------- 1 | import <%= props.name %>Actions, <%= props.name %>Reducer as reducer, INITIAL_STATE from "./<%= props.name %>Reducers"; 2 | 3 | it("attempt", () => { 4 | const state = reducer(INITIAL_STATE, <%= props.name %>Actions.request('data')) 5 | 6 | expect(state.fetching).toBe(true) 7 | }) 8 | 9 | it("success", () => { 10 | const state = reducer(INITIAL_STATE, <%= props.name %>Actions.success('hi')) 11 | 12 | expect(state.payload).toBe('hi') 13 | }) 14 | 15 | it("failure", () => { 16 | const state = reducer(INITIAL_STATE, <%= props.name %>Actions.failure()) 17 | 18 | expect(state.fetching).toBe(false) 19 | expect(state.error).toBe(true) 20 | }) 21 | -------------------------------------------------------------------------------- /boilerplate/App/Components/AlertMessage/AlertMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, View, ViewStyle } from "react-native"; 3 | import styles from "./AlertMessageStyles"; 4 | 5 | interface Props { 6 | icon?: string; 7 | show?: boolean; 8 | style?: ViewStyle; 9 | title?: string; 10 | } 11 | 12 | const AlertMessage: React.SFC = ({ icon, show = true, style, title }: Props) => show ? ( 13 | 14 | 15 | {title && title.toUpperCase()} 16 | 17 | 18 | ) : null; 19 | 20 | export default AlertMessage; 21 | -------------------------------------------------------------------------------- /boilerplate/App/Components/RoundedButton/RoundedButtonStyles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import Colors from "../../Themes/Colors"; 3 | import Fonts from "../../Themes/Fonts"; 4 | import Metrics from "../../Themes/Metrics"; 5 | 6 | export default StyleSheet.create({ 7 | button: { 8 | height: 45, 9 | borderRadius: 5, 10 | marginHorizontal: Metrics.section, 11 | marginVertical: Metrics.baseMargin, 12 | backgroundColor: Colors.fire, 13 | justifyContent: "center", 14 | }, 15 | buttonText: { 16 | color: Colors.snow, 17 | textAlign: "center", 18 | fontWeight: "bold", 19 | fontSize: Fonts.size.medium, 20 | marginVertical: Metrics.baseMargin, 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /boilerplate/App/Components/AlertMessage/AlertMessageStyles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { Colors, Fonts, Metrics } from "../../Themes/"; 3 | 4 | export default StyleSheet.create({ 5 | container: { 6 | justifyContent: "center", 7 | marginVertical: Metrics.section, 8 | }, 9 | contentContainer: { 10 | alignSelf: "center", 11 | alignItems: "center", 12 | }, 13 | message: { 14 | marginTop: Metrics.baseMargin, 15 | marginHorizontal: Metrics.baseMargin, 16 | textAlign: "center", 17 | fontFamily: Fonts.type.base, 18 | fontSize: Fonts.size.regular, 19 | fontWeight: "bold", 20 | color: Colors.steel, 21 | }, 22 | icon: { 23 | color: Colors.steel, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /boilerplate/App/Themes/Colors.ts: -------------------------------------------------------------------------------- 1 | const colors = { 2 | background: "#1F0808", 3 | clear: "rgba(0,0,0,0)", 4 | facebook: "#3b5998", 5 | transparent: "rgba(0,0,0,0)", 6 | silver: "#F7F7F7", 7 | steel: "#CCCCCC", 8 | error: "rgba(200, 0, 0, 0.8)", 9 | ricePaper: "rgba(255,255,255, 0.75)", 10 | frost: "#D8D8D8", 11 | cloud: "rgba(200,200,200, 0.35)", 12 | windowTint: "rgba(0, 0, 0, 0.4)", 13 | panther: "#161616", 14 | charcoal: "#595959", 15 | coal: "#2d2d2d", 16 | bloodOrange: "#fb5f26", 17 | snow: "white", 18 | ember: "rgba(164, 0, 48, 0.5)", 19 | fire: "#e73536", 20 | drawer: "rgba(30, 30, 29, 0.95)", 21 | eggplant: "#251a34", 22 | border: "#483F53", 23 | banner: "#5F3E63", 24 | text: "#E0D7E5", 25 | }; 26 | 27 | export default colors; 28 | -------------------------------------------------------------------------------- /boilerplate/App/Config/ReduxPersist.ts: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from "react-native"; 2 | import immutablePersistenceTransform from "../Services/ImmutablePersistenceTransform"; 3 | 4 | // More info here: https://shift.infinite.red/shipping-persistant-reducers-7341691232b1 5 | const REDUX_PERSIST = { 6 | active: false, 7 | reducerVersion: "1.0", 8 | storeConfig: { 9 | storage: AsyncStorage, 10 | blacklist: ["login", "search", "nav"], // reducer keys that you do NOT want stored to persistence here 11 | // whitelist: [], Optionally, just specify the keys you DO want stored to 12 | // persistence. An empty array means 'don't store any reducers' -> infinitered/ignite#409 13 | transforms: [immutablePersistenceTransform], 14 | }, 15 | }; 16 | 17 | export default REDUX_PERSIST; 18 | -------------------------------------------------------------------------------- /boilerplate/App/Components/DrawerButton/DrawerButtonTest.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { shallow } from "enzyme"; 3 | import * as React from "react"; 4 | import "react-native"; 5 | import * as renderer from "react-test-renderer"; 6 | import DrawerButton from "./DrawerButton"; 7 | 8 | test("AlertMessage component renders correctly", () => { 9 | const tree = renderer.create( {}} text="hi" />).toJSON(); 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | 13 | test("onPress", () => { 14 | let i = 0; 15 | const onPress = () => i++; 16 | const wrapperPress = shallow(); 17 | 18 | expect(wrapperPress.prop("onPress")).toBe(onPress); // uses the right handler 19 | expect(i).toBe(0); 20 | wrapperPress.simulate("press"); 21 | expect(i).toBe(1); 22 | }); 23 | -------------------------------------------------------------------------------- /boilerplate/App/Themes/Metrics.ts: -------------------------------------------------------------------------------- 1 | import {Dimensions, Platform} from "react-native"; 2 | 3 | const { width, height } = Dimensions.get("window"); 4 | 5 | // Used via Metrics.baseMargin 6 | const metrics = { 7 | marginHorizontal: 10, 8 | marginVertical: 10, 9 | section: 25, 10 | baseMargin: 10, 11 | doubleBaseMargin: 20, 12 | smallMargin: 5, 13 | doubleSection: 50, 14 | horizontalLineHeight: 1, 15 | screenWidth: width < height ? width : height, 16 | screenHeight: width < height ? height : width, 17 | navBarHeight: (Platform.OS === "ios") ? 64 : 54, 18 | buttonRadius: 4, 19 | icons: { 20 | tiny: 15, 21 | small: 20, 22 | medium: 30, 23 | large: 45, 24 | xl: 50, 25 | }, 26 | images: { 27 | small: 20, 28 | medium: 40, 29 | large: 60, 30 | logo: 200, 31 | }, 32 | }; 33 | 34 | export default metrics; 35 | -------------------------------------------------------------------------------- /boilerplate/App/Components/FullButton/FullButtonTest.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { shallow } from "enzyme"; 3 | import * as React from "react"; 4 | import "react-native"; 5 | import * as renderer from "react-test-renderer"; 6 | import FullButton from "./FullButton"; 7 | 8 | test("FullButton component renders correctly", () => { 9 | const tree = renderer.create( {}} text="hi" />).toJSON(); 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | 13 | test("onPress", () => { 14 | let i = 0; // i guess i could have used sinon here too... less is more i guess 15 | const onPress = () => i++; 16 | const wrapperPress = shallow(); 17 | 18 | expect(wrapperPress.prop("onPress")).toBe(onPress); // uses the right handler 19 | expect(i).toBe(0); 20 | wrapperPress.simulate("press"); 21 | expect(i).toBe(1); 22 | }); 23 | -------------------------------------------------------------------------------- /boilerplate/App/Config/ReactotronConfig.ts: -------------------------------------------------------------------------------- 1 | import Reactotron from "reactotron-react-native"; 2 | import { reactotronRedux as reduxPlugin } from "reactotron-redux"; 3 | import sagaPlugin from "reactotron-redux-saga"; 4 | import ImmutableObject from "seamless-immutable"; 5 | import Config from "../Config/DebugConfig"; 6 | 7 | if (Config.useReactotron) { 8 | // https://github.com/infinitered/reactotron for more options! 9 | Reactotron 10 | .configure({ name: "Ignite App" }) 11 | .useReactNative() 12 | .use(reduxPlugin({ onRestore: ImmutableObject })) 13 | .use(sagaPlugin()) 14 | .connect(); 15 | 16 | // Let's clear Reactotron on every time we load the app 17 | Reactotron.clear(); 18 | 19 | // Totally hacky, but this allows you to not both importing reactotron-react-native 20 | // on every file. This is just DEV mode, so no big deal. 21 | (console as any).tron = Reactotron; 22 | } 23 | -------------------------------------------------------------------------------- /boilerplate/Tests/Setup.tsx.ejs: -------------------------------------------------------------------------------- 1 | /// 2 | // Mock your external modules here if needed 3 | <%_ if (props.i18n === 'react-native-i18n') { _%> 4 | jest 5 | .mock('react-native-i18n', () => { 6 | const english = require('../App/I18n/languages/english.json') 7 | const keys = require('ramda') 8 | const replace = require('ramda') 9 | const forEach = require('ramda') 10 | 11 | return { 12 | t: (key, replacements) => { 13 | let value = english[key] 14 | if (!value) return key 15 | if (!replacements) return value 16 | 17 | forEach((r) => { 18 | value = replace(`{{${r}}}`, replacements[r], value) 19 | }, keys(replacements)) 20 | return value 21 | } 22 | } 23 | }) 24 | <%_ } else { _%> 25 | // jest 26 | // .mock('react-native-device-info', () => { 27 | // return { isTablet: jest.fn(() => { return false }) } 28 | // }) 29 | <%_ } _%> 30 | -------------------------------------------------------------------------------- /boilerplate/App/Components/AlertMessage/AlertMessageTest.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import * as React from "react"; 3 | import "react-native"; 4 | import * as renderer from "react-test-renderer"; 5 | import AlertMessage from "./AlertMessage"; 6 | 7 | test("AlertMessage component renders correctly if show is true", () => { 8 | const tree = renderer.create().toJSON(); 9 | expect(tree).toMatchSnapshot(); 10 | }); 11 | 12 | test("AlertMessage component does not render if show is false", () => { 13 | const tree = renderer.create().toJSON(); 14 | expect(tree).toMatchSnapshot(); 15 | }); 16 | 17 | test("AlertMessage component renders correctly if backgroundColor prop is set", () => { 18 | const tree = renderer.create().toJSON(); 19 | expect(tree).toMatchSnapshot(); 20 | }); 21 | -------------------------------------------------------------------------------- /templates/flatlist-grid-style.ejs: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native' 2 | import { ApplicationStyles, Metrics, Colors } from '../../../Themes' 3 | 4 | export default StyleSheet.create({ 5 | ...ApplicationStyles.screen, 6 | container: { 7 | flex: 1, 8 | backgroundColor: Colors.background 9 | }, 10 | row: { 11 | flex: 1, 12 | backgroundColor: Colors.fire, 13 | marginVertical: Metrics.smallMargin, 14 | justifyContent: 'center', 15 | margin: 10, 16 | padding: 5, 17 | paddingVertical: 10, 18 | borderRadius: Metrics.smallMargin 19 | }, 20 | boldLabel: { 21 | fontWeight: 'bold', 22 | alignSelf: 'center', 23 | color: Colors.snow, 24 | textAlign: 'center', 25 | marginBottom: Metrics.smallMargin 26 | }, 27 | label: { 28 | textAlign: 'center', 29 | color: Colors.snow 30 | }, 31 | listContent: { 32 | marginTop: Metrics.baseMargin 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /boilerplate/App/Services/ImmutablePersistenceTransform.tsx: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | import * as SeamlessImmutable from "seamless-immutable"; 3 | 4 | // is this object already Immutable? 5 | const isImmutable = R.has("asMutable"); 6 | 7 | // change this Immutable object into a JS object 8 | const convertToJs = (state: any) => state.asMutable({deep: true}); 9 | 10 | // optionally convert this object into a JS object if it is Immutable 11 | const fromImmutable = R.when(isImmutable, convertToJs); 12 | 13 | // convert this JS object into an Immutable object 14 | const toImmutable = (raw: any) => SeamlessImmutable(raw); 15 | 16 | // the transform interface that redux-persist is expecting 17 | export default { 18 | out: (state: any) => { 19 | // console.log({ retrieving: state }) 20 | return toImmutable(state); 21 | }, 22 | in: (raw: any) => { 23 | // console.log({ storing: raw }) 24 | return fromImmutable(raw); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /boilerplate/App/Sagas/GithubSagas/index.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponse } from "apisauce"; 2 | import { path } from "ramda"; 3 | import { SagaIterator } from "redux-saga"; 4 | import { call, put } from "redux-saga/effects"; 5 | import { GithubAction, GithubActions } from "../../Reducers/GithubReducers"; 6 | import { GithubApi, GithubResponse, GithubUser } from "../../Services/GithubApi"; 7 | 8 | export function * getUserAvatar(api: GithubApi, action: GithubAction): SagaIterator { 9 | const { payload } = action; 10 | // make the call to the api 11 | const response: ApiResponse = yield call(api.getUser, payload.username); 12 | 13 | if (response.ok) { 14 | const firstUser = path(["data", "items"], response)[0]; 15 | const avatar = firstUser.avatar_url; 16 | // do data conversion here if needed 17 | yield put(GithubActions.userSuccess({avatar})); 18 | } else { 19 | yield put(GithubActions.userFailure()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /commands/saga.js: -------------------------------------------------------------------------------- 1 | // @cliDescription Generates a saga with an optional test. 2 | 3 | module.exports = async function (context) { 4 | // grab some features 5 | const { parameters, ignite, print, strings } = context 6 | const { pascalCase, isBlank } = strings 7 | const config = ignite.loadIgniteConfig() 8 | const { tests } = config 9 | 10 | // validation 11 | if (isBlank(parameters.first)) { 12 | print.info(`${context.runtime.brand} generate saga \n`) 13 | print.info('A name is required.') 14 | return 15 | } 16 | 17 | const name = pascalCase(parameters.first) 18 | const props = { name } 19 | 20 | const jobs = [{ template: `saga.ejs`, target: `App/Sagas/${name}Sagas/index.ts` }] 21 | if (tests) { 22 | jobs.push({ 23 | template: `saga-test-${tests}.ejs`, 24 | target: `App/Sagas/${name}Sagas/${name}SagaTest.ts` 25 | }) 26 | } 27 | 28 | // make the templates 29 | await ignite.copyBatch(context, jobs, props) 30 | } 31 | -------------------------------------------------------------------------------- /commands/reducers.js: -------------------------------------------------------------------------------- 1 | // @cliDescription Generates a action/creator/reducer set for Redux. 2 | 3 | module.exports = async function (context) { 4 | // grab some features 5 | const { parameters, ignite, strings, print } = context 6 | const { isBlank, pascalCase } = strings 7 | const config = ignite.loadIgniteConfig() 8 | 9 | // validation 10 | if (isBlank(parameters.first)) { 11 | print.info(`${context.runtime.brand} generate reducers \n`) 12 | print.info('A name is required.') 13 | return 14 | } 15 | 16 | const name = pascalCase(parameters.first) 17 | const props = { name } 18 | 19 | const jobs = [{ template: `reducers.ejs`, target: `App/Reducers/${name}Reducers/index.tsx` }] 20 | if (config.tests && config.tests !== 'none') { 21 | jobs.push({ 22 | template: `reducers-test-${config.tests}.ejs`, 23 | target: `App/Reducers/${name}Reducers/${name}ReducersTest.tsx` 24 | }) 25 | } 26 | 27 | await ignite.copyBatch(context, jobs, props) 28 | } 29 | -------------------------------------------------------------------------------- /boilerplate/App/Reducers/GithubReducers/GithubReducersTest.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { GithubActions, GithubReducer, INITIAL_STATE } from "./index"; 3 | 4 | test("request", () => { 5 | const username = "taco"; 6 | const state = GithubReducer(INITIAL_STATE, GithubActions.userRequest({username})); 7 | 8 | expect(state.fetching).toBe(true); 9 | expect(state.username).toBe(username); 10 | expect(state.avatar).toBeNull(); 11 | }); 12 | 13 | test("success", () => { 14 | const avatar = "http://placekitten.com/200/300"; 15 | const state = GithubReducer(INITIAL_STATE, GithubActions.userSuccess({avatar})); 16 | 17 | expect(state.fetching).toBe(false); 18 | expect(state.avatar).toBe(avatar); 19 | expect(state.error).toBeNull(); 20 | }); 21 | 22 | test("failure", () => { 23 | const state = GithubReducer(INITIAL_STATE, GithubActions.userFailure()); 24 | 25 | expect(state.fetching).toBe(false); 26 | expect(state.error).toBe(true); 27 | expect(state.avatar).toBeNull(); 28 | }); 29 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/README.md: -------------------------------------------------------------------------------- 1 | ### Containers Folder 2 | A container is what they call a "Smart Component" in Redux. It is a component 3 | which knows about Redux. They are usually used as "Screens". 4 | 5 | Also located in here are 2 special containers: `App.js` and `RootContainer.js`. 6 | 7 | `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). 8 | 9 | `RootContainer.js` is the first visual component in the app. It is the ancestor of all other screens and components. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /lib/react-native-version.js: -------------------------------------------------------------------------------- 1 | const { pathOr, is } = require('ramda') 2 | 3 | // the default React Native version for this boilerplate 4 | const REACT_NATIVE_VERSION = '0.51.0' 5 | 6 | // where the version lives under gluegun 7 | const pathToVersion = ['parameters', 'options', 'react-native-version'] 8 | 9 | // accepts the context and returns back the version 10 | const getVersionFromContext = pathOr(REACT_NATIVE_VERSION, pathToVersion) 11 | 12 | /** 13 | * Gets the React Native version to use. 14 | * 15 | * Attempts to read it from the command line, and if not there, falls back 16 | * to the version we want for this boilerplate. For example: 17 | * 18 | * $ ignite new Custom --react-native-version 0.44.1 19 | * 20 | * @param {*} context - The gluegun context. 21 | */ 22 | const getReactNativeVersion = (context = {}) => { 23 | const version = getVersionFromContext(context) 24 | return is(String, version) ? version : REACT_NATIVE_VERSION 25 | } 26 | 27 | module.exports = { 28 | REACT_NATIVE_VERSION, 29 | getReactNativeVersion 30 | } 31 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Provider } from "react-redux"; 3 | import Reactotron from "reactotron-react-native"; 4 | import "../Config"; 5 | import DebugConfig from "../Config/DebugConfig"; 6 | import createStore from "../Reducers"; 7 | import RootContainer from "./RootContainer"; 8 | 9 | // create our store 10 | const store = createStore(); 11 | 12 | /** 13 | * Provides an entry point into our application. Both index.ios.js and index.android.js 14 | * call this component first. 15 | * 16 | * We create our Redux store here, put it into a provider and then bring in our 17 | * RootContainer. 18 | * 19 | * We separate like this to play nice with React Native's hot reloading. 20 | */ 21 | class App extends React.Component { 22 | public render() { 23 | return ( 24 | 25 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | // allow reactotron overlay for fast design in dev mode 32 | export default DebugConfig.useReactotron 33 | ? Reactotron.overlay(App) 34 | : App; 35 | -------------------------------------------------------------------------------- /boilerplate/App/Services/FixtureApi.tsx: -------------------------------------------------------------------------------- 1 | import {ApiResponse} from "apisauce"; 2 | import {GithubApi, GithubResponse} from "./GithubApi"; 3 | 4 | export default { 5 | // Functions return fixtures 6 | getRoot: (): Promise> => { 7 | return Promise.resolve({ 8 | ok: true, 9 | data: require("../Fixtures/root.json"), 10 | } as ApiResponse); 11 | }, 12 | getRate: (): Promise> => { 13 | return Promise.resolve({ 14 | ok: true, 15 | data: require("../Fixtures/rateLimit.json"), 16 | } as ApiResponse); 17 | }, 18 | getUser: (username: string): Promise> => { 19 | // This fixture only supports gantman or else returns skellock 20 | const gantmanData = require("../Fixtures/gantman.json"); 21 | const skellockData = require("../Fixtures/skellock.json"); 22 | return Promise.resolve({ 23 | ok: true, 24 | data: username.toLowerCase() === "gantman" ? gantmanData : skellockData, 25 | } as ApiResponse); 26 | }, 27 | } as GithubApi; 28 | -------------------------------------------------------------------------------- /boilerplate/App/Themes/Fonts.ts: -------------------------------------------------------------------------------- 1 | const type = { 2 | base: "Avenir-Book", 3 | bold: "Avenir-Black", 4 | emphasis: "HelveticaNeue-Italic", 5 | }; 6 | 7 | const size = { 8 | h1: 38, 9 | h2: 34, 10 | h3: 30, 11 | h4: 26, 12 | h5: 20, 13 | h6: 19, 14 | input: 18, 15 | regular: 17, 16 | medium: 14, 17 | small: 12, 18 | tiny: 8.5, 19 | }; 20 | 21 | const style = { 22 | h1: { 23 | fontFamily: type.base, 24 | fontSize: size.h1, 25 | }, 26 | h2: { 27 | fontWeight: "bold", 28 | fontSize: size.h2, 29 | }, 30 | h3: { 31 | fontFamily: type.emphasis, 32 | fontSize: size.h3, 33 | }, 34 | h4: { 35 | fontFamily: type.base, 36 | fontSize: size.h4, 37 | }, 38 | h5: { 39 | fontFamily: type.base, 40 | fontSize: size.h5, 41 | }, 42 | h6: { 43 | fontFamily: type.emphasis, 44 | fontSize: size.h6, 45 | }, 46 | normal: { 47 | fontFamily: type.base, 48 | fontSize: size.regular, 49 | }, 50 | description: { 51 | fontFamily: type.base, 52 | fontSize: size.medium, 53 | }, 54 | }; 55 | 56 | export default { 57 | type, 58 | size, 59 | style, 60 | }; 61 | -------------------------------------------------------------------------------- /boilerplate/App/Components/RoundedButton/RoundedButtonTest.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import { shallow } from "enzyme"; 3 | import * as React from "react"; 4 | import "react-native"; 5 | import * as renderer from "react-test-renderer"; 6 | import RoundedButton from "./RoundedButton"; 7 | 8 | test("RoundedButton component renders correctly", () => { 9 | const tree = renderer.create( {}} text="howdy" />).toJSON(); 10 | expect(tree).toMatchSnapshot(); 11 | }); 12 | 13 | test("RoundedButton component with children renders correctly", () => { 14 | const tree = renderer.create( {}}>Howdy).toJSON(); 15 | expect(tree).toMatchSnapshot(); 16 | }); 17 | 18 | test("onPress", () => { 19 | let i = 0; // i guess i could have used sinon here too... less is more i guess 20 | const onPress = () => i++; 21 | const wrapperPress = shallow(); 22 | 23 | expect(wrapperPress.prop("onPress")).toBe(onPress); // uses the right handler 24 | expect(i).toBe(0); 25 | wrapperPress.simulate("press"); 26 | expect(i).toBe(1); 27 | }); 28 | -------------------------------------------------------------------------------- /boilerplate/App/Components/DrawerButton/DrawerButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, TouchableOpacity } from "react-native"; 3 | import ExamplesRegistry from "../../Services/ExamplesRegistry"; 4 | import styles from "./DrawerButtonStyles"; 5 | 6 | // Note that this file (App/Components/DrawerButton) needs to be 7 | // imported in your app somewhere, otherwise your component won't be 8 | // compiled and added to the examples dev screen. 9 | 10 | // Ignore in coverage report 11 | /* istanbul ignore next */ 12 | ExamplesRegistry.addComponentExample("Drawer Button", () => 13 | ( 14 | window.alert("Your drawers are showing")} 18 | />), 19 | ); 20 | 21 | interface Props { 22 | onPress: () => void; 23 | text: string; 24 | } 25 | 26 | // tslint:disable-next-line:no-empty 27 | const DrawerButton: React.SFC = ({text, onPress = () => {}}: Props) => ( 28 | 29 | {text} 30 | 31 | ); 32 | 33 | export default DrawerButton; 34 | -------------------------------------------------------------------------------- /templates/listview-grid-style.ejs: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { ApplicationStyles, Metrics, Colors } from "../../../Themes"; 3 | 4 | export default StyleSheet.create({ 5 | ...ApplicationStyles.screen, 6 | container: { 7 | flex: 1, 8 | backgroundColor: Colors.background 9 | }, 10 | row: { 11 | width: 100, 12 | height: 100, 13 | justifyContent: 'center', 14 | alignItems: 'center', 15 | margin: Metrics.baseMargin, 16 | backgroundColor: Colors.fire, 17 | borderRadius: Metrics.smallMargin 18 | }, 19 | sectionHeader: { 20 | paddingTop: Metrics.doubleBaseMargin, 21 | width: Metrics.screenWidth, 22 | alignSelf: 'center', 23 | margin: Metrics.baseMargin, 24 | backgroundColor: Colors.background 25 | }, 26 | boldLabel: { 27 | fontWeight: 'bold', 28 | alignSelf: 'center', 29 | color: Colors.snow, 30 | textAlign: 'center', 31 | marginBottom: Metrics.smallMargin 32 | }, 33 | label: { 34 | alignSelf: 'center', 35 | color: Colors.snow, 36 | textAlign: 'center' 37 | }, 38 | listContent: { 39 | justifyContent: 'space-around', 40 | flexDirection: 'row', 41 | flexWrap: 'wrap' 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /boilerplate/App/Components/FullButton/FullButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, TouchableOpacity, ViewStyle } from "react-native"; 3 | import ExamplesRegistry from "../../Services/ExamplesRegistry"; 4 | import styles from "./FullButtonStyles"; 5 | 6 | // Note that this file (App/Components/FullButton) needs to be 7 | // imported in your app somewhere, otherwise your component won't be 8 | // compiled and added to the examples dev screen. 9 | 10 | // Ignore in coverage report 11 | /* istanbul ignore next */ 12 | ExamplesRegistry.addComponentExample("Full Button", () => ( 13 | window.alert("Full Button Pressed!")} 17 | />), 18 | ); 19 | 20 | interface Props { 21 | onPress?: () => void; 22 | style?: ViewStyle; 23 | text?: string; 24 | } 25 | 26 | // tslint:disable-next-line:no-empty 27 | const FullButton: React.SFC = ({text, style, onPress = () => {}}: Props) => ( 28 | 29 | {text && text.toUpperCase()} 30 | 31 | ); 32 | 33 | export default FullButton; 34 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/RootContainer/RootContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StatusBar, View } from "react-native"; 3 | import { connect } from "react-redux"; 4 | import ReduxPersist from "../../Config/ReduxPersist"; 5 | import ReduxNavigation from "../../Navigation/ReduxNavigation"; 6 | import { StartupActions } from "../../Reducers/StartupReducers"; 7 | 8 | // Styles 9 | import styles from "./RootContainerStyles"; 10 | 11 | interface Props { 12 | startup: () => void; 13 | } 14 | 15 | interface State { 16 | 17 | } 18 | 19 | export class RootContainer extends React.Component { 20 | public componentDidMount() { 21 | // if redux persist is not active fire startup action 22 | if (!ReduxPersist.active) { 23 | this.props.startup(); 24 | } 25 | } 26 | 27 | public render() { 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | } 36 | 37 | const mapDispatchToProps = (dispatch: any): Props => ({ 38 | startup: () => dispatch(StartupActions.startup()), 39 | }); 40 | 41 | export default connect(null, mapDispatchToProps)(RootContainer); 42 | -------------------------------------------------------------------------------- /boilerplate/App/Fixtures/skellock.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 1, 3 | "incomplete_results": false, 4 | "items": [ 5 | { 6 | "login": "skellock", 7 | "id": 68273, 8 | "avatar_url": "https://avatars.githubusercontent.com/u/68273?v=3", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/skellock", 11 | "html_url": "https://github.com/skellock", 12 | "followers_url": "https://api.github.com/users/skellock/followers", 13 | "following_url": "https://api.github.com/users/skellock/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/skellock/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/skellock/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/skellock/subscriptions", 17 | "organizations_url": "https://api.github.com/users/skellock/orgs", 18 | "repos_url": "https://api.github.com/users/skellock/repos", 19 | "events_url": "https://api.github.com/users/skellock/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/skellock/received_events", 21 | "type": "User", 22 | "site_admin": false, 23 | "score": 107.22611 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /boilerplate/App/Lib/ReduxHelpers.ts: -------------------------------------------------------------------------------- 1 | import { ActionCreator, AnyAction, Reducer, ReducersMapObject } from "redux"; 2 | import { createAction, EmptyAction, FluxStandardAction, getType, PayloadAction, TypeGetter } from "typesafe-actions"; 3 | 4 | export type FluxActionCreator FluxStandardAction = any> = AC & TypeGetter; 5 | 6 | type ActionCreatorMap = { 7 | readonly [P in keyof T]: FluxActionCreator

; 8 | }; 9 | 10 | export type ReducerMap = { 11 | readonly [T in keyof A]: Reducer; 12 | }; 13 | 14 | export function mapReducers( 15 | initialState: S, 16 | reducers: R, 17 | actionCreators: ActionCreatorMap): Reducer { 18 | const reducerMap = new Map(Object.entries(actionCreators).map(([key, val]): [string, Reducer] => 19 | [getType(val), reducers[key]])); 20 | 21 | return (state: S = initialState, action: AnyAction) => { 22 | if (!("type" in action)) { 23 | return state; 24 | } 25 | const reducer = reducerMap.get(action.type); 26 | if (!reducer) { 27 | return state; 28 | } 29 | return reducer(state, action); 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /boilerplate/App/Reducers/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { combineReducers } from "redux"; 3 | import root from "../Sagas"; 4 | import configureStore from "./CreateStore"; 5 | import { GithubReducer, ImmutableGithubState } from "./GithubReducers"; 6 | import { NavigationReducer, NavigationState } from "./NavigationReducers"; 7 | 8 | /* ------------- Assemble The Reducers ------------- */ 9 | export const reducers = combineReducers({ 10 | nav: NavigationReducer, 11 | github: GithubReducer, 12 | }); 13 | 14 | export interface State { 15 | github: ImmutableGithubState; 16 | nav: NavigationState; 17 | } 18 | 19 | export default () => { 20 | // tslint:disable-next-line:prefer-const 21 | let { store, sagasManager, sagaMiddleware } = configureStore(reducers, root); 22 | 23 | if (module.hot) { 24 | module.hot.accept(() => { 25 | const nextRootReducer = require("./").reducers; 26 | store.replaceReducer(nextRootReducer); 27 | 28 | const newYieldedSagas = require("../Sagas").default; 29 | sagasManager.cancel(); 30 | sagasManager.done.then(() => { 31 | sagasManager = sagaMiddleware.run(newYieldedSagas); 32 | }); 33 | }); 34 | } 35 | 36 | return store; 37 | }; 38 | -------------------------------------------------------------------------------- /boilerplate/App/Themes/Images.ts: -------------------------------------------------------------------------------- 1 | // leave off @2x/@3x 2 | const images = { 3 | logo: require("../Images/ir.png"), 4 | clearLogo: require("../Images/top_logo.png"), 5 | launch: require("../Images/launch-icon.png"), 6 | ready: require("../Images/your-app.png"), 7 | ignite: require("../Images/ignite_logo.png"), 8 | igniteClear: require("../Images/ignite-logo-transparent.png"), 9 | tileBg: require("../Images/tile_bg.png"), 10 | background: require("../Images/BG.png"), 11 | buttonBackground: require("../Images/button-bg.png"), 12 | api: require("../Images/Icons/icon-api-testing.png"), 13 | components: require("../Images/Icons/icon-components.png"), 14 | deviceInfo: require("../Images/Icons/icon-device-information.png"), 15 | faq: require("../Images/Icons/faq-icon.png"), 16 | home: require("../Images/Icons/icon-home.png"), 17 | theme: require("../Images/Icons/icon-theme.png"), 18 | usageExamples: require("../Images/Icons/icon-usage-examples.png"), 19 | chevronRight: require("../Images/Icons/chevron-right.png"), 20 | hamburger: require("../Images/Icons/hamburger.png"), 21 | backButton: require("../Images/Icons/back-button.png"), 22 | closeButton: require("../Images/Icons/close-button.png"), 23 | }; 24 | 25 | export default images; 26 | -------------------------------------------------------------------------------- /boilerplate/App/Sagas/index.ts: -------------------------------------------------------------------------------- 1 | import { all, takeLatest } from "redux-saga/effects"; 2 | import { getType } from "typesafe-actions"; 3 | 4 | import DebugConfig from "../Config/DebugConfig"; 5 | import FixtureAPI from "../Services/FixtureApi"; 6 | import {createAPI, GithubApi} from "../Services/GithubApi"; 7 | 8 | /* ------------- Types ------------- */ 9 | 10 | import { GithubActions } from "../Reducers/GithubReducers"; 11 | import { StartupActions } from "../Reducers/StartupReducers"; 12 | 13 | /* ------------- Sagas ------------- */ 14 | 15 | import { getUserAvatar } from "./GithubSagas"; 16 | import { startup } from "./StartupSagas"; 17 | 18 | /* ------------- API ------------- */ 19 | 20 | // The API we use is only used from Sagas, so we create it here and pass along 21 | // to the sagas which need it. 22 | const api = DebugConfig.useFixtures ? FixtureAPI : createAPI(); 23 | 24 | /* ------------- Connect Types To Sagas ------------- */ 25 | 26 | export default function * root() { 27 | yield all([ 28 | // some sagas only receive an action 29 | takeLatest(getType(StartupActions.startup), startup), 30 | 31 | // some sagas receive extra parameters in addition to an action 32 | takeLatest(getType(GithubActions.userRequest), getUserAvatar, api), 33 | ]); 34 | } 35 | -------------------------------------------------------------------------------- /boilerplate/App/Reducers/ScreenTrackingMiddleware.tsx: -------------------------------------------------------------------------------- 1 | import { NavigationActions } from "react-navigation"; 2 | import Reactotron from "reactotron-react-native"; 3 | 4 | // gets the current screen from navigation state 5 | const getCurrentRouteName = (navigationState) => { 6 | if (!navigationState) { 7 | return null; 8 | } 9 | const route = navigationState.routes[navigationState.index]; 10 | // dive into nested navigators 11 | if (route.routes) { 12 | return getCurrentRouteName(route); 13 | } 14 | return route.routeName; 15 | }; 16 | 17 | const screenTracking = ({ getState }) => (next) => (action) => { 18 | if ( 19 | action.type !== NavigationActions.NAVIGATE && 20 | action.type !== NavigationActions.BACK 21 | ) { 22 | return next(action); 23 | } 24 | 25 | const currentScreen = getCurrentRouteName(getState().nav); 26 | const result = next(action); 27 | const nextScreen = getCurrentRouteName(getState().nav); 28 | if (nextScreen !== currentScreen) { 29 | try { 30 | Reactotron.log(`NAVIGATING ${currentScreen} to ${nextScreen}`); 31 | // Example: Analytics.trackEvent('user_navigation', {currentScreen, nextScreen}) 32 | } catch (e) { 33 | Reactotron.log(e); 34 | } 35 | } 36 | return result; 37 | }; 38 | 39 | export default screenTracking; 40 | -------------------------------------------------------------------------------- /boilerplate/App/Components/RoundedButton/RoundedButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, TouchableOpacity } from "react-native"; 3 | import ExamplesRegistry from "../../Services/ExamplesRegistry"; 4 | import styles from "./RoundedButtonStyles"; 5 | 6 | // Note that this file (App/Components/RoundedButton) needs to be 7 | // imported in your app somewhere, otherwise your component won't be 8 | // compiled and added to the examples dev screen. 9 | 10 | // Ignore in coverage report 11 | /* istanbul ignore next */ 12 | ExamplesRegistry.addComponentExample("Rounded Button", () => 13 | ( 14 | window.alert("Rounded Button Pressed!")} 18 | />), 19 | ); 20 | 21 | interface Props { 22 | onPress?: () => any; 23 | text?: string; 24 | children?: string; 25 | } 26 | 27 | // tslint:disable-next-line:no-empty 28 | const RoundedButton: React.SFC = ({ text, children, onPress = () => { } }: Props) => { 29 | const buttonText = (text || children || "").toUpperCase(); 30 | return ( 31 | 32 | {buttonText} 33 | 34 | ); 35 | }; 36 | 37 | export default RoundedButton; 38 | -------------------------------------------------------------------------------- /boilerplate/App/Containers/LaunchScreen/LaunchScreen.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Image, ScrollView, Text, View } from "react-native"; 3 | import DevscreensButton from "../../../ignite/DevScreens/DevscreensButton"; 4 | 5 | import { Images } from "../../Themes"; 6 | 7 | // Styles 8 | import styles from "./LaunchScreenStyles"; 9 | 10 | export default class LaunchScreen extends Component { 11 | public render() { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | This probably isn't what your app is going to look like. Unless your designer 24 | handed you this screen and, in that case, congrats! You're ready to ship. 25 | For everyone else, this is where you'll see a live preview of your fully functioning app using Ignite. 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /boilerplate/types/@storybook/react-native.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for @storybook/react 3.0 2 | // Project: https://github.com/storybooks/storybook 3 | // Definitions by: Joscha Feth 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | // TypeScript Version: 2.3 6 | 7 | /// 8 | 9 | import * as React from 'react'; 10 | 11 | export type Renderable = React.ComponentType | JSX.Element; 12 | export type RenderFunction = () => Renderable; 13 | 14 | export type StoryDecorator = (story: RenderFunction, context: { kind: string, story: string }) => Renderable | null; 15 | 16 | export interface Story { 17 | readonly kind: string; 18 | add(storyName: string, callback: RenderFunction): this; 19 | addDecorator(decorator: StoryDecorator): this; 20 | } 21 | 22 | export function addDecorator(decorator: StoryDecorator): void; 23 | export function configure(fn: () => void, module?: NodeModule): void; 24 | export function setAddon(addon: object): void; 25 | export function storiesOf(name: string, module?: NodeModule): Story; 26 | export function storiesOf(name: string, module?: NodeModule): Story & T; 27 | 28 | export interface StoryObject { 29 | name: string; 30 | render: RenderFunction; 31 | } 32 | 33 | export interface StoryBucket { 34 | kind: string; 35 | stories: StoryObject[]; 36 | } 37 | 38 | export function getStorybook(): StoryBucket[]; 39 | -------------------------------------------------------------------------------- /boilerplate/App/Services/RehydrationServices.tsx: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from "react-native"; 2 | import { persistStore } from "redux-persist"; 3 | import DebugConfig from "../Config/DebugConfig"; 4 | import ReduxPersist from "../Config/ReduxPersist"; 5 | import StartupActions from "../Redux/StartupRedux"; 6 | 7 | const updateReducers = (store) => { 8 | const reducerVersion = ReduxPersist.reducerVersion; 9 | const config = ReduxPersist.storeConfig; 10 | const startup = () => store.dispatch(StartupActions.startup()); 11 | 12 | // Check to ensure latest reducer version 13 | AsyncStorage.getItem("reducerVersion").then((localVersion) => { 14 | if (localVersion !== reducerVersion) { 15 | if (DebugConfig.useReactotron) { 16 | console.tron.display({ 17 | name: "PURGE", 18 | value: { 19 | "Old Version:": localVersion, 20 | "New Version:": reducerVersion, 21 | }, 22 | preview: "Reducer Version Change Detected", 23 | important: true, 24 | }); 25 | } 26 | // Purge store 27 | persistStore(store, config, startup).purge(); 28 | AsyncStorage.setItem("reducerVersion", reducerVersion); 29 | } else { 30 | persistStore(store, config, startup); 31 | } 32 | }).catch(() => { 33 | persistStore(store, config, startup); 34 | AsyncStorage.setItem("reducerVersion", reducerVersion); 35 | }); 36 | }; 37 | 38 | export default {updateReducers}; 39 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The questions to ask during the install process. 3 | */ 4 | const questions = [ 5 | { 6 | name: 'dev-screens', 7 | message: 'Would you like Ignite Development Screens?', 8 | type: 'list', 9 | choices: ['No', 'Yes'] 10 | }, 11 | { 12 | name: 'vector-icons', 13 | message: 'What vector icon library will you use?', 14 | type: 'list', 15 | choices: ['none', 'react-native-vector-icons'] 16 | }, 17 | { 18 | name: 'i18n', 19 | message: 'What internationalization library will you use?', 20 | type: 'list', 21 | choices: ['none', 'react-native-i18n'] 22 | }, 23 | { 24 | name: 'animatable', 25 | message: 'What animation library will you use?', 26 | type: 'list', 27 | choices: ['none', 'react-native-animatable'] 28 | }, 29 | { 30 | name: 'tests', 31 | message: 'What test library will you use?', 32 | type: 'list', 33 | choices: ['none', 'jest'] 34 | } 35 | ] 36 | 37 | /** 38 | * The max preset. 39 | */ 40 | const max = { 41 | 'dev-screens': 'Yes', 42 | 'vector-icons': 'react-native-vector-icons', 43 | i18n: 'react-native-i18n', 44 | animatable: 'react-native-animatable', 45 | tests: 'jest' 46 | } 47 | 48 | /** 49 | * The min preset. 50 | */ 51 | const min = { 52 | 'dev-screens': 'No', 53 | 'vector-icons': 'none', 54 | i18n: 'none', 55 | animatable: 'none', 56 | tests: 'none' 57 | } 58 | 59 | module.exports = { 60 | questions, 61 | answers: { min, max } 62 | } 63 | -------------------------------------------------------------------------------- /boilerplate/App/Services/FixtureAPITest.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | import * as R from "ramda"; 3 | import FixtureAPI from "../../App/Services/FixtureApi"; 4 | import API from "../../App/Services/GithubApi"; 5 | 6 | test("All fixtures map to actual API", () => { 7 | const fixtureKeys = R.keys(FixtureAPI).sort(); 8 | const apiKeys = R.keys(API.createAPI()); 9 | 10 | const intersection = R.intersection(fixtureKeys, apiKeys).sort(); 11 | 12 | // There is no difference between the intersection and all fixtures 13 | expect(R.equals(fixtureKeys, intersection)).toBe(true); 14 | }); 15 | 16 | test("FixtureAPI getRate returns the right file", () => { 17 | const expectedFile = require("../../App/Fixtures/rateLimit.json"); 18 | 19 | return FixtureAPI.getRate().then((data) => expect(data).toEqual({ 20 | ok: true, 21 | data: expectedFile, 22 | })); 23 | }); 24 | 25 | test("FixtureAPI getUser returns the right file for gantman", () => { 26 | const expectedFile = require("../../App/Fixtures/gantman.json"); 27 | return FixtureAPI.getUser("GantMan").then((data) => expect(data).toEqual({ 28 | ok: true, 29 | data: expectedFile, 30 | })); 31 | }); 32 | 33 | test("FixtureAPI getUser returns the right file for skellock as default", () => { 34 | const expectedFile = require("../../App/Fixtures/skellock.json"); 35 | return FixtureAPI.getUser("Whatever").then((data) => expect(data).toEqual({ 36 | ok: true, 37 | data: expectedFile, 38 | })); 39 | }); 40 | -------------------------------------------------------------------------------- /boilerplate/types/reduxsauce/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Action, AnyAction, Reducer, ReducersMapObject } from "redux"; 2 | export interface ActionTypes { 3 | [key: string]: string; 4 | } 5 | 6 | export interface ActionConfig { 7 | [key: string]: string[] | ActionCreator | {[key: string]: any} | null; 8 | } 9 | 10 | export type ActionCreator = (...args: any[]) => Action; 11 | 12 | export interface ActionCreators { 13 | [key: string]: ActionCreator; 14 | } 15 | 16 | export function createActions(config: ActionConfig, options?: {prefix?: string}): {Types: ActionTypes, Creators: ActionCreators }; 17 | 18 | /** 19 | * Creates a reducer. 20 | * @param {object} initialState - The initial state for this reducer. 21 | * @param {object} handlers - Keys are action types (strings), values are reducers (functions). 22 | * @return {Reducer} A reducer object. 23 | */ 24 | export function createReducer(initialState: S, handlers: ReducersMapObject): Reducer; 25 | 26 | export function createTypes(types: string, options?: {prefix?: string, [key: string]: any}): ActionTypes; 27 | 28 | /** 29 | * Allows your reducers to be reset. 30 | * 31 | * @param {string} typeToReset - The action type to listen for. 32 | * @param {Reducer} originalReducer - The reducer to wrap. 33 | */ 34 | export function resettableReducer(type: string, originalReducer: Reducer): Reducer; 35 | 36 | export function resettableReducer(type: string): (originalReducer: Reducer) => Reducer; 37 | -------------------------------------------------------------------------------- /templates/container.ejs: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, View } from "react-native"; 3 | import { connect } from "react-redux"; 4 | import * as Redux from "redux"; 5 | import { RootState } from "../../Reducers"; 6 | import { Images } from "../../Themes"; 7 | import Metrics from "../../Themes/Metrics"; 8 | 9 | // Styles 10 | import styles from "./<%= props.name %>Style"; 11 | 12 | /** 13 | * The properties passed to the component 14 | */ 15 | export interface OwnProps { 16 | 17 | } 18 | /** 19 | * The properties mapped from Redux dispatch 20 | */ 21 | export interface DispatchProps { 22 | 23 | } 24 | 25 | /** 26 | * The properties mapped from the global state 27 | */ 28 | export interface StateProps { 29 | 30 | } 31 | 32 | /** 33 | * The local state 34 | */ 35 | export interface State { 36 | 37 | } 38 | 39 | type Props = StateProps & DispatchProps & OwnProps; 40 | 41 | class <%= props.name %> extends 42 | React.Component { 43 | public state = { 44 | 45 | } 46 | 47 | public render() { 48 | return ( 49 | 50 | Hello <%= props.name %> 51 | 52 | ); 53 | } 54 | } 55 | 56 | const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => ({ 57 | 58 | }); 59 | 60 | const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => { 61 | return {}; 62 | }; 63 | 64 | export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) as React.ComponentClass; 65 | -------------------------------------------------------------------------------- /commands/component.js: -------------------------------------------------------------------------------- 1 | // @cliDescription Generates a stateless component, styles, and an optional test. 2 | 3 | module.exports = async function (context) { 4 | // grab some features 5 | const { parameters, strings, print, ignite } = context 6 | const { pascalCase, isBlank } = strings 7 | const config = ignite.loadIgniteConfig() 8 | const { tests } = config 9 | 10 | // validation 11 | if (isBlank(parameters.first)) { 12 | print.info(`${context.runtime.brand} generate component \n`) 13 | print.info('A name is required.') 14 | return 15 | } 16 | 17 | // read some configuration 18 | const name = pascalCase(parameters.first) 19 | const props = { name } 20 | const jobs = [ 21 | { 22 | template: 'component.ejs', 23 | target: `App/Components/${name}/${name}.tsx` 24 | }, 25 | { 26 | template: 'component-style.ejs', 27 | target: `App/Components/${name}/${name}Style.ts` 28 | }, 29 | { 30 | template: 'component-index.ejs', 31 | target: `App/Components/${name}/index.ts` 32 | }, 33 | { 34 | template: 'component-story.ejs', 35 | target: `App/Components/${name}/${name}.story.tsx` 36 | }, 37 | tests === 'ava' && 38 | { 39 | template: 'component-test-ava.ejs', 40 | target: `App/Components/${name}/${name}Test.tsx` 41 | }, 42 | tests === 'jest' && 43 | { 44 | template: 'component-test-jest.ejs', 45 | target: `App/Components/${name}/${name}Test.tsx` 46 | } 47 | ] 48 | 49 | await ignite.copyBatch(context, jobs, props) 50 | } 51 | -------------------------------------------------------------------------------- /boilerplate/App/Sagas/GithubSagas/GithubSagaTest.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { path } from "ramda"; 3 | import { call, put } from "redux-saga/effects"; 4 | import { GithubActions } from "../../Reducers/GithubReducers"; 5 | import FixtureAPI from "../../Services/FixtureApi"; 6 | import { getUserAvatar } from "./index"; 7 | 8 | const stepper = (fn) => (mock?) => fn.next(mock).value; 9 | 10 | test("first calls API", () => { 11 | const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"}))); 12 | // first yield is API 13 | expect(step()).toEqual(call(FixtureAPI.getUser, "ascorbic")); 14 | }); 15 | 16 | test("success path", () => { 17 | FixtureAPI.getUser("ascorbic").then((response) => { 18 | 19 | // tslint:disable-next-line:max-line-length 20 | const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"}))); 21 | // first step API 22 | step(); 23 | // Second step successful return 24 | const stepResponse = step(response); 25 | // Get the avatar Url from the response 26 | const firstUser = path(["data", "items"], response)[0]; 27 | const avatar = firstUser.avatar_url; 28 | expect(stepResponse).toEqual(put(GithubActions.userSuccess(avatar))); 29 | }); 30 | }); 31 | 32 | test("failure path", () => { 33 | const response = {ok: false}; 34 | const step = stepper(getUserAvatar(FixtureAPI, GithubActions.userRequest({username: "ascorbic"}))); 35 | // first step API 36 | step(); 37 | // Second step failed response 38 | expect(step(response)).toEqual(put(GithubActions.userFailure())); 39 | }); 40 | -------------------------------------------------------------------------------- /templates/saga-test-jest.ejs: -------------------------------------------------------------------------------- 1 | /* *********************************************************** 2 | * Wiring Instructions 3 | * To make this test work, you'll need to: 4 | * - Add a Fixture named get<%= props.name %> to the 5 | * ./App/Services/FixtureApi file. You can just keep adding 6 | * functions to that file. 7 | *************************************************************/ 8 | 9 | import FixtureAPI from '../../Services/FixtureApi' 10 | import { call, put } from 'redux-saga/effects' 11 | import { get<%= props.name %> } from './index' 12 | import <%= props.name %>Actions from '../../Reducers/<%= props.name %>Reducers' 13 | 14 | const stepper = (fn) => (mock) => fn.next(mock).value 15 | 16 | it('first calls API', () => { 17 | const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) 18 | // first yield is the API 19 | expect(step()).toEqual(call(FixtureAPI.get<%= props.name %>, 'taco')) 20 | }) 21 | 22 | it('success path', () => { 23 | const response = FixtureAPI.get<%= props.name %>('taco') 24 | const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) 25 | // Step 1: Hit the api 26 | step() 27 | // Step 2: Successful return and data! 28 | expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Success(21))) 29 | }) 30 | 31 | it('failure path', () => { 32 | const response = {ok: false} 33 | const step = stepper(get<%= props.name %>(FixtureAPI, {data: 'taco'})) 34 | // Step 1: Hit the api 35 | step() 36 | // Step 2: Failed response. 37 | expect(step(response)).toEqual(put(<%= pascalCase(props.name) %>Actions.<%= camelCase(props.name) %>Failure())) 38 | }) 39 | -------------------------------------------------------------------------------- /boilerplate/README.md: -------------------------------------------------------------------------------- 1 | # <%= props.name %> 2 | 3 | * TypeScript React Native App Utilizing [Ignite](https://github.com/infinitered/ignite) 4 | 5 | ## :arrow_up: How to Setup 6 | 7 | **Step 1:** git clone this repo: 8 | 9 | **Step 2:** cd to the cloned repo: 10 | 11 | **Step 3:** Install the Application with `yarn` or `npm i` 12 | 13 | 14 | ## :arrow_forward: How to Run App 15 | 16 | 1. cd to the repo 17 | 2. Run `npm run compile` 18 | 3. Run Build for either OS 19 | * for iOS 20 | * run `react-native run-ios` 21 | * for Android 22 | * Run Genymotion 23 | * run `react-native run-android` 24 | 25 | **To Lint on Commit** 26 | 27 | This is implemented using [husky](https://github.com/typicode/husky). There is no additional setup needed. 28 | 29 | ## :closed_lock_with_key: Secrets 30 | 31 | 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 32 | and other sensitive information in a `.env` file: 33 | 34 | ``` 35 | API_URL=https://myapi.com 36 | GOOGLE_MAPS_API_KEY=abcdefgh 37 | ``` 38 | 39 | and access them from React Native like so: 40 | 41 | ``` 42 | import Secrets from 'react-native-config' 43 | 44 | Secrets.API_URL // 'https://myapi.com' 45 | Secrets.GOOGLE_MAPS_API_KEY // 'abcdefgh' 46 | ``` 47 | 48 | The `.env` file is ignored by git keeping those secrets out of your repo. 49 | 50 | ### Get started: 51 | 1. Copy .env.example to .env 52 | 2. Add your config variables 53 | 3. Follow instructions at [https://github.com/luggit/react-native-config#setup](https://github.com/luggit/react-native-config#setup) 54 | 4. Done! 55 | -------------------------------------------------------------------------------- /templates/saga.ejs: -------------------------------------------------------------------------------- 1 | /* *********************************************************** 2 | * A short word on how to use this automagically generated file. 3 | * We're often asked in the ignite gitter channel how to connect 4 | * to a to a third party api, so we thought we'd demonstrate - but 5 | * you should know you can use sagas for other flow control too. 6 | * 7 | * Other points: 8 | * - You'll need to add this saga to sagas/index.ts 9 | * - This template uses the api declared in sagas/index.ts, so 10 | * you'll need to define a constant in that file. 11 | *************************************************************/ 12 | import { ApiResponse } from "apisauce"; 13 | import { SagaIterator } from "redux-saga"; 14 | import { call, put } from "redux-saga/effects"; 15 | import { <%= props.name %>Actions, <%= props.name %>Action } from "../../Reducers/<%= props.name %>Reducers"; 16 | import { <%= props.name %>Api, <%= props.name %>Response } from "../../Services/<%= props.name %>Api"; 17 | 18 | 19 | export function * get<%= props.name %> (api: <%= props.name %>Api, action: <%= props.name %>Action): SagaIterator { 20 | const { data } = action; 21 | // make the call to the api 22 | const response: ApiResponse<<%= props.name %>Response> = yield call(api.get<%= camelCase(props.name) %>, data); 23 | 24 | // success? 25 | if (response.ok) { 26 | // You might need to change the response here - do this with a 'transform', 27 | // located in ../../Transforms/. Otherwise, just pass the data back from the api. 28 | yield put(<%= props.name %>Actions.success({data: response.data})); 29 | } else { 30 | yield put(<%= props.name %>Actions.failure()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /boilerplate/App/Reducers/CreateStore.tsx: -------------------------------------------------------------------------------- 1 | import Reactotron from "reactotron-react-native"; 2 | import { applyMiddleware, compose, createStore, Reducer } from "redux"; 3 | import sagaMiddlewareFactory, { Monitor, SagaIterator } from "redux-saga"; 4 | import Config from "../Config/DebugConfig"; 5 | import ScreenTracking from "./ScreenTrackingMiddleware"; 6 | 7 | // creates the store 8 | export default (rootReducer: Reducer, rootSaga: () => SagaIterator) => { 9 | /* ------------- Redux Configuration ------------- */ 10 | 11 | const middleware = []; 12 | const enhancers = []; 13 | 14 | /* ------------- Analytics Middleware ------------- */ 15 | if (Config.useReactotron) { 16 | middleware.push(ScreenTracking); 17 | } 18 | /* ------------- Saga Middleware ------------- */ 19 | 20 | let opts = {}; 21 | if (Config.useReactotron) { 22 | const sagaMonitor: Monitor = Reactotron.createSagaMonitor(); 23 | opts = { sagaMonitor }; 24 | } 25 | const sagaMiddleware = sagaMiddlewareFactory(opts); 26 | middleware.push(sagaMiddleware); 27 | 28 | /* ------------- Assemble Middleware ------------- */ 29 | 30 | enhancers.push(applyMiddleware(...middleware)); 31 | 32 | // if Reactotron is enabled (default for __DEV__), we'll create the store through Reactotron 33 | const createAppropriateStore = Config.useReactotron ? Reactotron.createStore : createStore; 34 | 35 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 36 | 37 | const store = createAppropriateStore(rootReducer, composeEnhancers(...enhancers)); 38 | 39 | // kick off root saga 40 | const sagasManager = sagaMiddleware.run(rootSaga); 41 | 42 | return { 43 | store, 44 | sagasManager, 45 | sagaMiddleware, 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /boilerplate/App/Sagas/StartupSagas/index.ts: -------------------------------------------------------------------------------- 1 | import { is } from "ramda"; 2 | import Reactotron from "reactotron-react-native"; 3 | import { Action } from "redux"; 4 | import { SagaIterator } from "redux-saga"; 5 | import { put, select } from "redux-saga/effects"; 6 | import { GithubAction, GithubActions } from "../../Reducers/GithubReducers"; 7 | import { StartupActions } from "../../Reducers/StartupReducers"; 8 | 9 | // exported to make available for tests 10 | export const selectAvatar = (state: any) => state.github.avatar; 11 | 12 | // process STARTUP actions 13 | export function * startup(action?: Action): SagaIterator { 14 | if (__DEV__) { 15 | // straight-up string logging 16 | Reactotron.log("Hello, I'm an example of how to log via Reactotron."); 17 | Reactotron.log(action); 18 | // logging an object for better clarity 19 | Reactotron.log({ 20 | message: "pass objects for better logging", 21 | someGeneratorFunction: selectAvatar, 22 | }); 23 | 24 | // fully customized! 25 | const subObject = { a: 1, b: [1, 2, 3], c: true, circularDependency: undefined as any}; 26 | subObject.circularDependency = subObject; // osnap! 27 | Reactotron.display({ 28 | name: "🔥 IGNITE 🔥", 29 | preview: "You should totally expand this", 30 | value: { 31 | "💃": "Welcome to the future!", 32 | subObject, 33 | "someInlineFunction": () => true, 34 | "someGeneratorFunction": startup, 35 | "someNormalFunction": selectAvatar, 36 | }, 37 | }); 38 | } 39 | const avatar = yield select(selectAvatar); 40 | // only get if we don't have it yet 41 | if (!is(String, avatar)) { 42 | yield put(GithubActions.userRequest({username: "ascorbic"})); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /boilerplate/App/Services/ExamplesRegistry.tsx: -------------------------------------------------------------------------------- 1 | import R from "ramda"; 2 | import * as React from "react"; 3 | import { Text, View } from "react-native"; 4 | import DebugConfig from "../Config/DebugConfig"; 5 | import { ApplicationStyles } from "../Themes"; 6 | const globalComponentExamplesRegistry = []; 7 | const globalPluginExamplesRegistry = []; 8 | 9 | export const addComponentExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalComponentExamplesRegistry.push({title, usage}); }; // eslint-disable-line 10 | 11 | export const addPluginExample = (title, usage = () => {}) => { if (DebugConfig.includeExamples) globalPluginExamplesRegistry.push({title, usage}); }; // eslint-disable-line 12 | 13 | const renderComponentExample = (example) => { 14 | return ( 15 | 16 | 17 | {example.title} 18 | 19 | {example.usage.call()} 20 | 21 | ); 22 | }; 23 | 24 | const renderPluginExample = (example) => { 25 | return ( 26 | 27 | 28 | {example.title} 29 | 30 | {example.usage.call()} 31 | 32 | ); 33 | }; 34 | 35 | export const renderComponentExamples = () => R.map(renderComponentExample, globalComponentExamplesRegistry); 36 | 37 | export const renderPluginExamples = () => R.map(renderPluginExample, globalPluginExamplesRegistry); 38 | 39 | // Default for readability 40 | export default { 41 | renderComponentExamples, 42 | addComponentExample, 43 | renderPluginExamples, 44 | addPluginExample, 45 | }; 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ignite-typescript-boilerplate", 3 | "description": "TypeScript boilerplate for React Native.", 4 | "license": "MIT", 5 | "repository": "aerian-studios/ignite-typescript-boilerplate", 6 | "homepage": "https://github.com/aerian-studios/ignite-typescript-boilerplate", 7 | "version": "0.1.5", 8 | "files": [ 9 | "boilerplate", 10 | "commands", 11 | "lib", 12 | "templates", 13 | "boilerplate.js", 14 | "ignite.json", 15 | "options.js", 16 | "readme.md", 17 | "plugin.js" 18 | ], 19 | "author": { 20 | "name": "Aerian Studios", 21 | "email": "matt.kane@aerian.com", 22 | "url": "https://github.com/aerian-studios/ignite-typescript-boilerplate" 23 | }, 24 | "scripts": { 25 | "lint": "tslint", 26 | "test": "jest", 27 | "watch": "jest --runInBand --watch", 28 | "coverage": "jest --runInBand --coverage", 29 | "shipit": "np" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^21.1.5", 33 | "@types/ramda": "^0.24.18", 34 | "@types/react": "^16.0.18", 35 | "@types/react-native": "^0.49.2", 36 | "@types/react-navigation": "^1.0.21", 37 | "@types/react-redux": "^5.0.10", 38 | "@types/redux": "^3.6.0", 39 | "@types/seamless-immutable": "^7.1.1", 40 | "@types/webpack-env": "^1.13.2", 41 | "fs-jetpack": "^1.0.0", 42 | "jest": "^20.0.4", 43 | "np": "^2.15.0", 44 | "react": "16.0.0-beta.5", 45 | "redux": "^3.7.2", 46 | "redux-saga": "^0.16.0", 47 | "reduxsauce": "^0.7.0", 48 | "seamless-immutable": "^7.1.2", 49 | "sinon": "^2.3.1", 50 | "tempy": "^0.1.0", 51 | "tslint": "^5.8.0", 52 | "tslint-react": "^3.2.0", 53 | "typesafe-actions": "^1.1.2", 54 | "typescript": "^2.6.1" 55 | }, 56 | "dependencies": { 57 | "ramda": "^0.23.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/react-native-version.test.js: -------------------------------------------------------------------------------- 1 | const boilerplate = require('../lib/react-native-version') 2 | 3 | // grab a few things from the boilerplate module 4 | const get = boilerplate.getReactNativeVersion 5 | const DEFAULT = boilerplate.REACT_NATIVE_VERSION 6 | 7 | /** 8 | * Runs with a valid gluegun context and a staged version number. 9 | * 10 | * @param {*} reactNativeVersion The React Native version to use. 11 | * @return {string} The version number we should be using. 12 | */ 13 | const mock = reactNativeVersion => get({ 14 | parameters: { 15 | options: { 16 | 'react-native-version': reactNativeVersion 17 | } 18 | } 19 | }) 20 | 21 | // this would only happen if we screwed something up in our boilerplate.js 22 | test('it handles strange inputs from code', () => { 23 | expect(get()).toBe(DEFAULT) 24 | expect(get(null)).toBe(DEFAULT) 25 | expect(get(true)).toBe(DEFAULT) 26 | expect(get(8)).toBe(DEFAULT) 27 | expect(get('hello')).toBe(DEFAULT) 28 | expect(get([])).toBe(DEFAULT) 29 | expect(get({})).toBe(DEFAULT) 30 | expect(get(() => true)).toBe(DEFAULT) 31 | }) 32 | 33 | // this could happen because it's valid input via minimist from the user 34 | test('it handles strange input from the user', () => { 35 | expect(mock(true)).toBe(DEFAULT) 36 | expect(mock(false)).toBe(DEFAULT) 37 | expect(mock([])).toBe(DEFAULT) 38 | expect(mock({})).toBe(DEFAULT) 39 | }) 40 | 41 | // very edge-casey 42 | test('it handles not-quite semver numbers', () => { 43 | expect(mock(0)).toBe(DEFAULT) 44 | expect(mock(0.25)).toBe(DEFAULT) 45 | }) 46 | 47 | // happy path 48 | test('it handles valid versions', () => { 49 | expect(mock('0.41.0')).toBe('0.41.0') 50 | expect(mock('0.41.0-beta.1')).toBe('0.41.0-beta.1') 51 | expect(mock(DEFAULT)).toBe(DEFAULT) 52 | expect(mock('next')).toBe('next') 53 | }) 54 | -------------------------------------------------------------------------------- /templates/screen.ejs: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Alert, Image, Text, TouchableOpacity, View } from "react-native"; 3 | import Icon from "react-native-vector-icons/FontAwesome"; 4 | import { NavigationAction, NavigationDrawerScreenOptions, NavigationScreenProps, NavigationState } from "react-navigation"; 5 | import { connect } from "react-redux"; 6 | import * as Redux from "redux"; 7 | import { RootState } from "../../Reducers"; 8 | import { Images } from "../../Themes"; 9 | import Metrics from "../../Themes/Metrics"; 10 | 11 | // Styles 12 | import styles from "./<%= props.name %>Style"; 13 | 14 | /** 15 | * The properties passed to the component 16 | */ 17 | export interface OwnProps { 18 | 19 | } 20 | /** 21 | * The properties mapped from Redux dispatch 22 | */ 23 | export interface DispatchProps { 24 | 25 | } 26 | 27 | /** 28 | * The properties mapped from the global state 29 | */ 30 | export interface StateProps { 31 | 32 | } 33 | 34 | /** 35 | * The local state 36 | */ 37 | export interface State { 38 | 39 | } 40 | 41 | type Props = StateProps & DispatchProps & OwnProps & NavigationScreenProps<{}>; 42 | 43 | class <%= props.name %> extends 44 | React.Component { 45 | 46 | public state = { 47 | 48 | }; 49 | 50 | public static navigationOptions: NavigationDrawerScreenOptions = { 51 | drawerLabel: "Welcome", 52 | drawerIcon: ({ tintColor, focused }: {tintColor: string, focused: boolean}) => ( 53 | 58 | ), 59 | }; 60 | 61 | public render() { 62 | return ( 63 | 64 | Hello <%= props.name %> 65 | 66 | ); 67 | } 68 | } 69 | 70 | const mapDispatchToProps = (dispatch: Redux.Dispatch): DispatchProps => ({ 71 | 72 | }); 73 | 74 | const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => { 75 | return {}; 76 | }; 77 | 78 | export default connect(mapStateToProps, mapDispatchToProps)(<%= props.name %>) as React.ComponentClass; 79 | -------------------------------------------------------------------------------- /commands/container.js: -------------------------------------------------------------------------------- 1 | // @cliDescription Generates a redux smart component. 2 | 3 | const patterns = require('../lib/patterns') 4 | 5 | module.exports = async function (context) { 6 | // grab some features 7 | const { parameters, strings, print, ignite, filesystem } = context 8 | const { pascalCase, isBlank } = strings 9 | const config = ignite.loadIgniteConfig() 10 | 11 | // validation 12 | if (isBlank(parameters.first)) { 13 | print.info(`${context.runtime.brand} generate container \n`) 14 | print.info('A name is required.') 15 | return 16 | } 17 | 18 | const name = pascalCase(parameters.first) 19 | const props = { name } 20 | 21 | const jobs = [ 22 | { 23 | template: 'container.ejs', 24 | target: `App/Containers/${name}/${name}.tsx` 25 | }, 26 | { 27 | template: 'container-style.ejs', 28 | target: `App/Containers/${name}/${name}Style.ts` 29 | } 30 | ] 31 | 32 | await ignite.copyBatch(context, jobs, props) 33 | 34 | // if using `react-navigation` go the extra step 35 | // and insert the container into the nav router 36 | if (config.navigation === 'react-navigation') { 37 | const containerName = name 38 | const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx` 39 | const importToAdd = `import ${containerName} from "../Containers/${containerName}";` 40 | const routeToAdd = ` ${containerName}: { screen: ${containerName} },` 41 | 42 | if (!filesystem.exists(appNavFilePath)) { 43 | const msg = `No '${appNavFilePath}' file found. Can't insert container.` 44 | print.error(msg) 45 | process.exit(1) 46 | } 47 | 48 | // insert container import 49 | ignite.patchInFile(appNavFilePath, { 50 | after: patterns[patterns.constants.PATTERN_IMPORTS], 51 | insert: importToAdd 52 | }) 53 | 54 | // insert container route 55 | ignite.patchInFile(appNavFilePath, { 56 | after: patterns[patterns.constants.PATTERN_ROUTES], 57 | insert: routeToAdd 58 | }) 59 | } else { 60 | print.info('Container created, manually add it to your navigation') 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /boilerplate/App/Fixtures/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "current_user_url": "https://api.github.com/user", 3 | "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}", 4 | "authorizations_url": "https://api.github.com/authorizations", 5 | "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}", 6 | "commit_search_url": "https://api.github.com/search/commits?q={query}{&page,per_page,sort,order}", 7 | "emails_url": "https://api.github.com/user/emails", 8 | "emojis_url": "https://api.github.com/emojis", 9 | "events_url": "https://api.github.com/events", 10 | "feeds_url": "https://api.github.com/feeds", 11 | "followers_url": "https://api.github.com/user/followers", 12 | "following_url": "https://api.github.com/user/following{/target}", 13 | "gists_url": "https://api.github.com/gists{/gist_id}", 14 | "hub_url": "https://api.github.com/hub", 15 | "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}", 16 | "issues_url": "https://api.github.com/issues", 17 | "keys_url": "https://api.github.com/user/keys", 18 | "notifications_url": "https://api.github.com/notifications", 19 | "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}", 20 | "organization_url": "https://api.github.com/orgs/{org}", 21 | "public_gists_url": "https://api.github.com/gists/public", 22 | "rate_limit_url": "https://api.github.com/rate_limit", 23 | "repository_url": "https://api.github.com/repos/{owner}/{repo}", 24 | "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}", 25 | "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}", 26 | "starred_url": "https://api.github.com/user/starred{/owner}{/repo}", 27 | "starred_gists_url": "https://api.github.com/gists/starred", 28 | "team_url": "https://api.github.com/teams", 29 | "user_url": "https://api.github.com/users/{user}", 30 | "user_organizations_url": "https://api.github.com/user/orgs", 31 | "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", 32 | "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}" 33 | } -------------------------------------------------------------------------------- /commands/screen.js: -------------------------------------------------------------------------------- 1 | // @cliDescription Generates an opinionated container. 2 | 3 | const patterns = require('../lib/patterns') 4 | 5 | module.exports = async function (context) { 6 | // grab some features 7 | const { parameters, print, strings, ignite, filesystem } = context 8 | const { pascalCase, isBlank } = strings 9 | const config = ignite.loadIgniteConfig() 10 | 11 | // validation 12 | if (isBlank(parameters.first)) { 13 | print.info(`${context.runtime.brand} generate screen \n`) 14 | print.info('A name is required.') 15 | return 16 | } 17 | 18 | const name = pascalCase(parameters.first) 19 | const screenName = name.endsWith('Screen') ? name : `${name}Screen` 20 | const props = { name: screenName } 21 | 22 | const jobs = [ 23 | { 24 | template: `screen.ejs`, 25 | target: `App/Containers/${screenName}/${screenName}.tsx` 26 | }, 27 | { 28 | template: `screen-style.ejs`, 29 | target: `App/Containers/${screenName}/${screenName}Style.tsx` 30 | }, 31 | { 32 | template: 'component-index.ejs', 33 | target: `App/Containers/${screenName}/index.ts` 34 | } 35 | ] 36 | 37 | // make the templates 38 | await ignite.copyBatch(context, jobs, props) 39 | 40 | // if using `react-navigation` go the extra step 41 | // and insert the screen into the nav router 42 | if (config.navigation === 'react-navigation') { 43 | const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx` 44 | const importToAdd = `import ${screenName} from "../Containers/${screenName}";` 45 | const routeToAdd = ` ${screenName}: { screen: ${screenName} },` 46 | 47 | if (!filesystem.exists(appNavFilePath)) { 48 | const msg = `No '${appNavFilePath}' file found. Can't insert screen.` 49 | print.error(msg) 50 | process.exit(1) 51 | } 52 | 53 | // insert screen import 54 | ignite.patchInFile(appNavFilePath, { 55 | after: patterns[patterns.constants.PATTERN_IMPORTS], 56 | insert: importToAdd 57 | }) 58 | 59 | // insert screen route 60 | ignite.patchInFile(appNavFilePath, { 61 | after: patterns[patterns.constants.PATTERN_ROUTES], 62 | insert: routeToAdd 63 | }) 64 | } else { 65 | print.info(`Screen ${screenName} created, manually add it to your navigation`) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /boilerplate/App/Themes/ApplicationStyles.ts: -------------------------------------------------------------------------------- 1 | import { TextStyle, ViewStyle } from "react-native"; 2 | 3 | import Colors from "./Colors"; 4 | import Fonts from "./Fonts"; 5 | import Metrics from "./Metrics"; 6 | 7 | // This file is for a reusable grouping of Theme items. 8 | // Similar to an XML fragment layout in Android 9 | 10 | const ApplicationStyles = { 11 | screen: { 12 | mainContainer: { 13 | flex: 1, 14 | backgroundColor: Colors.transparent, 15 | } as ViewStyle, 16 | backgroundImage: { 17 | position: "absolute", 18 | top: 0, 19 | left: 0, 20 | bottom: 0, 21 | right: 0, 22 | } as ViewStyle, 23 | container: { 24 | flex: 1, 25 | paddingTop: Metrics.baseMargin, 26 | backgroundColor: Colors.transparent, 27 | } as ViewStyle, 28 | section: { 29 | margin: Metrics.section, 30 | padding: Metrics.baseMargin, 31 | } as ViewStyle, 32 | sectionText: { 33 | ...Fonts.style.normal, 34 | paddingVertical: Metrics.doubleBaseMargin, 35 | color: Colors.snow, 36 | marginVertical: Metrics.smallMargin, 37 | textAlign: "center", 38 | } as TextStyle, 39 | subtitle: { 40 | color: Colors.snow, 41 | padding: Metrics.smallMargin, 42 | marginBottom: Metrics.smallMargin, 43 | marginHorizontal: Metrics.smallMargin, 44 | } as TextStyle, 45 | titleText: { 46 | ...Fonts.style.h2, 47 | fontSize: 14, 48 | color: Colors.text, 49 | } as TextStyle, 50 | }, 51 | darkLabelContainer: { 52 | padding: Metrics.smallMargin, 53 | paddingBottom: Metrics.doubleBaseMargin, 54 | borderBottomColor: Colors.border, 55 | borderBottomWidth: 1, 56 | marginBottom: Metrics.baseMargin, 57 | } as ViewStyle, 58 | darkLabel: { 59 | fontFamily: Fonts.type.bold, 60 | color: Colors.snow, 61 | } as TextStyle, 62 | groupContainer: { 63 | margin: Metrics.smallMargin, 64 | flexDirection: "row", 65 | justifyContent: "space-around", 66 | alignItems: "center", 67 | } as ViewStyle, 68 | sectionTitle: { 69 | ...Fonts.style.h4, 70 | color: Colors.coal, 71 | backgroundColor: Colors.ricePaper, 72 | padding: Metrics.smallMargin, 73 | marginTop: Metrics.smallMargin, 74 | marginHorizontal: Metrics.baseMargin, 75 | borderWidth: 1, 76 | borderColor: Colors.ember, 77 | alignItems: "center", 78 | textAlign: "center", 79 | } as TextStyle, 80 | }; 81 | 82 | export default ApplicationStyles; 83 | -------------------------------------------------------------------------------- /boilerplate/App/Reducers/GithubReducers/index.tsx: -------------------------------------------------------------------------------- 1 | import { Action, AnyAction, Reducer } from "redux"; 2 | import * as SI from "seamless-immutable"; 3 | import { createAction, PayloadAction } from "typesafe-actions"; 4 | import { mapReducers, ReducerMap } from "../../Lib/ReduxHelpers"; 5 | 6 | /* ------------- Types and Action Creators ------------- */ 7 | interface RequestParams {username: string; } 8 | interface SuccessParams {avatar: string; } 9 | const actions = { 10 | userRequest: createAction("githubUserRequest", (params: RequestParams) => 11 | ({type: "githubUserRequest", payload: params})), 12 | userSuccess: createAction("githubUserSuccess", (params: SuccessParams) => 13 | ({type: "githubUserSuccess", payload: params})), 14 | userFailure: createAction("githubUserFailure"), 15 | }; 16 | 17 | export const GithubActions = actions; 18 | 19 | interface GithubState { 20 | avatar?: string | null; 21 | fetching?: boolean | null; 22 | error?: boolean | null; 23 | username?: string | null; 24 | } 25 | 26 | export type GithubAction = PayloadAction; 27 | 28 | export type ImmutableGithubState = SI.ImmutableObject; 29 | 30 | /* ------------- Initial State ------------- */ 31 | 32 | export const INITIAL_STATE: ImmutableGithubState = SI.from({ 33 | avatar: null, 34 | fetching: null, 35 | error: null, 36 | username: null, 37 | }); 38 | 39 | /* ------------- Reducers ------------- */ 40 | 41 | // request the avatar for a user 42 | export const userRequest: Reducer = 43 | (state: ImmutableGithubState, { payload }: AnyAction & {payload?: RequestParams}) => 44 | payload ? state.merge({ fetching: true, username: payload.username, avatar: null }) : state; 45 | 46 | // successful avatar lookup 47 | export const userSuccess: Reducer = 48 | (state: ImmutableGithubState, { payload }: AnyAction & {payload?: SuccessParams}) => 49 | payload ? state.merge({ fetching: false, error: null, avatar: payload.avatar }) : state; 50 | 51 | // failed to get the avatar 52 | export const userFailure: Reducer = (state: ImmutableGithubState) => 53 | state.merge({ fetching: false, error: true, avatar: null }); 54 | 55 | /* ------------- Hookup Reducers To Types ------------- */ 56 | 57 | const reducerMap: ReducerMap = { 58 | userRequest, 59 | userSuccess, 60 | userFailure, 61 | }; 62 | 63 | export const GithubReducer = mapReducers(INITIAL_STATE, reducerMap, actions); 64 | 65 | export default GithubReducer; 66 | -------------------------------------------------------------------------------- /templates/reducers.ejs: -------------------------------------------------------------------------------- 1 | import { Action, AnyAction, Reducer } from "redux"; 2 | import * as SI from "seamless-immutable"; 3 | import { createAction, PayloadAction } from "typesafe-actions"; 4 | import { mapReducers, ReducerMap } from "../../Lib/ReduxHelpers"; 5 | 6 | /* ------------- Types and Action Creators ------------- */ 7 | interface <%= pascalCase(props.name) %>SuccessParams {data: string; } 8 | 9 | const actionCreators = { 10 | request: createAction("<%= snakeCase(props.name).toUpperCase() %>_REQUEST"), 11 | success: (payload: <%= pascalCase(props.name) %>SuccessParams) => ({type: "<%= snakeCase(props.name).toUpperCase() %>_SUCCESS", payload})), 12 | failure: createAction("<%= snakeCase(props.name).toUpperCase() %>_FAILURE"), 13 | }; 14 | 15 | export const <%= pascalCase(props.name) %>Actions = actionCreators; 16 | 17 | export interface <%= pascalCase(props.name) %>State { 18 | data?: string | null; 19 | error?: boolean | null; 20 | fetching?: boolean | null; 21 | } 22 | 23 | export type <%= pascalCase(props.name) %>Action = PayloadActionState>; 24 | 25 | export type Immutable<%= pascalCase(props.name) %>State = SI.ImmutableObject<<%= pascalCase(props.name) %>State>; 26 | 27 | /* ------------- Initial State ------------- */ 28 | 29 | export const INITIAL_STATE: Immutable<%= pascalCase(props.name) %>State = SI.from({ 30 | data: null, 31 | error: null, 32 | fetching: null, 33 | }); 34 | 35 | /* ------------- Reducers ------------- */ 36 | 37 | export const request: ReducerState> = 38 | (state: Immutable<%= pascalCase(props.name) %>State) => 39 | state.merge({ fetching: true }); 40 | 41 | export const success: ReducerState> = 42 | (state: Immutable<%= pascalCase(props.name) %>State, action: AnyAction & {payload?: <%= pascalCase(props.name) %>SuccessParams}) => { 43 | if (!action.payload) { 44 | return failure(state, action); 45 | } 46 | const { data } = action.payload; 47 | 48 | return state.merge({ fetching: false, error: null, data }); 49 | }; 50 | 51 | export const failure: ReducerState> = (state: Immutable<%= pascalCase(props.name) %>State) => 52 | state.merge({ fetching: false, error: true, data: null }); 53 | 54 | /* ------------- Hookup Reducers To Types ------------- */ 55 | 56 | const reducerMap: ReducerMapState> = { 57 | request, 58 | failure, 59 | success, 60 | }; 61 | 62 | export const <%= pascalCase(props.name) %>Reducer = mapReducers(INITIAL_STATE, reducerMap, actionCreators); 63 | 64 | export default <%= pascalCase(props.name) %>Reducer; 65 | -------------------------------------------------------------------------------- /boilerplate/App/Services/GithubApi.tsx: -------------------------------------------------------------------------------- 1 | // a library to wrap and simplify api calls 2 | import {ApiResponse, create as apicreate} from "apisauce"; 3 | 4 | export interface GithubApi { 5 | getRoot: () => Promise>; 6 | getRate: () => Promise>; 7 | getUser: (username: string) => Promise>; 8 | } 9 | 10 | export interface GithubUser { 11 | login: string; 12 | id: number; 13 | avatar_url: string; 14 | gravatar_id: string; 15 | url: string; 16 | html_url: string; 17 | followers_url: string; 18 | following_url: string; 19 | gists_url: string; 20 | starred_url: string; 21 | subscriptions_url: string; 22 | organizations_url: string; 23 | repos_url: string; 24 | events_url: string; 25 | received_events_url: string; 26 | type: "User"; 27 | site_admin: boolean; 28 | score: number; 29 | } 30 | 31 | export interface GithubResponse { 32 | total_count: number; 33 | incomplete_results: false; 34 | items: GithubUser[]; 35 | } 36 | 37 | // our "constructor" 38 | export const createAPI = (baseURL = "https://api.github.com/"): GithubApi => { 39 | // ------ 40 | // STEP 1 41 | // ------ 42 | // 43 | // Create and configure an apisauce-based api object. 44 | // 45 | const api = apicreate({ 46 | // base URL is read from the "constructor" 47 | baseURL, 48 | // here are some default headers 49 | headers: { 50 | "Cache-Control": "no-cache", 51 | }, 52 | // 10 second timeout... 53 | timeout: 10000, 54 | }); 55 | 56 | // ------ 57 | // STEP 2 58 | // ------ 59 | // 60 | // Define some functions that call the api. The goal is to provide 61 | // a thin wrapper of the api layer providing nicer feeling functions 62 | // rather than "get", "post" and friends. 63 | // 64 | // I generally don't like wrapping the output at this level because 65 | // sometimes specific actions need to be take on `403` or `401`, etc. 66 | // 67 | // Since we can't hide from that, we embrace it by getting out of the 68 | // way at this level. 69 | // 70 | const getRoot = () => api.get(""); 71 | const getRate = () => api.get("rate_limit"); 72 | const getUser = (username: string) => api.get("search/users", {q: username}); 73 | 74 | // ------ 75 | // STEP 3 76 | // ------ 77 | // 78 | // Return back a collection of functions that we would consider our 79 | // interface. Most of the time it'll be just the list of all the 80 | // methods in step 2. 81 | // 82 | // Notice we're not returning back the `api` created in step 1? That's 83 | // because it is scoped privately. This is one way to create truly 84 | // private scoped goodies in JavaScript. 85 | // 86 | return { 87 | // a list of the API functions from step 2 88 | getRoot, 89 | getRate, 90 | getUser, 91 | }; 92 | }; 93 | 94 | // let's return back our create method as the default. 95 | export default { 96 | createAPI, 97 | }; 98 | -------------------------------------------------------------------------------- /templates/flatlist.ejs: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { View, Text, FlatList } from "react-native"; 3 | 4 | // More info here: https://facebook.github.io/react-native/docs/flatlist.html 5 | 6 | // Styles 7 | import styles from "./<%= props.name %>Style"; 8 | 9 | export interface RowItem { 10 | title: string; 11 | description: string; 12 | } 13 | interface Props { 14 | data: RowItem[]; 15 | } 16 | 17 | export default class <%= props.name %> extends React.PureComponent { 18 | 19 | /* *********************************************************** 20 | * `renderRow` function. How each cell/row should be rendered 21 | * It's our best practice to place a single component here: 22 | * 23 | * e.g. 24 | return 25 | *************************************************************/ 26 | renderRow ({item}: {item: RowItem}) { 27 | return ( 28 | 29 | {item.title} 30 | {item.description} 31 | 32 | ) 33 | } 34 | 35 | // Render a header? 36 | renderHeader = () => 37 | - Header - 38 | 39 | // Render a footer? 40 | renderFooter = () => 41 | - Footer - 42 | 43 | // Show this when data is empty 44 | renderEmpty = () => 45 | - Nothing to See Here - 46 | 47 | renderSeparator = () => 48 | - ~~~~~ - 49 | 50 | // The default function if no Key is provided is index 51 | // an identifiable key is important if you plan on 52 | // item reordering. Otherwise index is fine 53 | keyExtractor = (item: RowItem, index: number) => index 54 | 55 | // How many items should be kept im memory as we scroll? 56 | oneScreensWorth = 20 57 | 58 | // extraData is for anything that is not indicated in data 59 | // for instance, if you kept "favorites" in `this.state.favs` 60 | // pass that in, so changes in favorites will cause a re-render 61 | // and your renderItem will have access to change depending on state 62 | // e.g. `extraData`={this.state.favs} 63 | 64 | // Optimize your list if the height of each item can be calculated 65 | // by supplying a constant height, there is no need to measure each 66 | // item after it renders. This can save significant time for lists 67 | // of a size 100+ 68 | // e.g. itemLayout={(data, index) => ( 69 | // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} 70 | // )} 71 | 72 | render () { 73 | return ( 74 | 75 | 86 | 87 | ) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /templates/flatlist-grid.ejs: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { View, Text, FlatList } from "react-native"; 3 | import { connect } from "react-redux"; 4 | 5 | // More info here: https://facebook.github.io/react-native/docs/flatlist.html 6 | 7 | // Styles 8 | import styles from "./<%= props.name %>Style"; 9 | 10 | export interface RowItem { 11 | title: string; 12 | description: string; 13 | } 14 | interface Props { 15 | data: RowItem[]; 16 | } 17 | 18 | export default class <%= props.name %> extends React.PureComponent { 19 | 20 | /* *********************************************************** 21 | * `renderRow` function. How each cell/row should be rendered 22 | * It's our best practice to place a single component here: 23 | * 24 | * e.g. 25 | return 26 | *************************************************************/ 27 | renderRow ({item}: {item: RowItem}) { 28 | return ( 29 | 30 | {item.title} 31 | {item.description} 32 | 33 | ) 34 | } 35 | 36 | // Render a header? 37 | renderHeader = () => 38 | - Header - 39 | 40 | // Render a footer? 41 | renderFooter = () => 42 | - Footer - 43 | 44 | // Show this when data is empty 45 | renderEmpty = () => 46 | - Nothing to See Here - 47 | 48 | renderSeparator = () => 49 | - ~~~~~ - 50 | 51 | // The default function if no Key is provided is index 52 | // an identifiable key is important if you plan on 53 | // item reordering. Otherwise index is fine 54 | keyExtractor = (item: RowItem, index: number) => index 55 | 56 | // How many items should be kept im memory as we scroll? 57 | oneScreensWorth = 20 58 | 59 | // extraData is for anything that is not indicated in data 60 | // for instance, if you kept "favorites" in `this.state.favs` 61 | // pass that in, so changes in favorites will cause a re-render 62 | // and your renderItem will have access to change depending on state 63 | // e.g. `extraData`={this.state.favs} 64 | 65 | // Optimize your list if the height of each item can be calculated 66 | // by supplying a constant height, there is no need to measure each 67 | // item after it renders. This can save significant time for lists 68 | // of a size 100+ 69 | // e.g. itemLayout={(data, index) => ( 70 | // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} 71 | // )} 72 | 73 | render () { 74 | return ( 75 | 76 | 88 | 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /boilerplate/package.json.ejs: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "scripts": { 4 | "start": "node node_modules/react-native/local-cli/cli.js start", 5 | "test": "jest", 6 | "clean": "rm -rf $TMPDIR/react-* && watchman watch-del-all && npm cache clean --force", 7 | "clean:android": "cd android/ && ./gradlew clean && cd .. && react-native run-android", 8 | "newclear": "rm -rf $TMPDIR/react-* && watchman watch-del-all && rm -rf ios/build && rm -rf node_modules/ && npm cache clean --force && npm i", 9 | "test:watch": "jest --watch", 10 | "updateSnapshot": "jest --updateSnapshot", 11 | "coverage": "jest --coverage && open coverage/lcov-report/index.html || xdg-open coverage/lcov-report/index.html", 12 | "android:build": "cd android && ./gradlew assembleRelease", 13 | "android:install": "cd android && ./gradlew assembleRelease && ./gradlew installRelease", 14 | "android:hockeyapp": "cd android && ./gradlew assembleRelease && puck -submit=auto app/build/outputs/apk/app-release.apk", 15 | "android:devices": "$ANDROID_HOME/platform-tools/adb devices", 16 | "android:logcat": "$ANDROID_HOME/platform-tools/adb logcat *:S ReactNative:V ReactNativeJS:V", 17 | "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", 18 | "storybook": "storybook start -p 7007", 19 | "lint": "tslint --project . -e '**/*.js' -t verbose | snazzy", 20 | "lintdiff": "git diff --name-only --cached --relative | grep '\\.tsx?$' | xargs tslint | snazzy", 21 | "fixcode": "tslint --project . -e '**/*.js' --fix", 22 | "git-hook": "npm run lint -s && npm run test -s" 23 | }, 24 | "dependencies": { 25 | "apisauce": "^0.14.0", 26 | "format-json": "^1.0.3", 27 | "lodash": "^4.17.2", 28 | "querystringify": "0.0.4", 29 | "ramda": "^0.24.1", 30 | "react-native-config": "^0.6.0", 31 | "react-native-drawer": "^2.3.0", 32 | "react-navigation": "^1.0.0-beta.7", 33 | "react-redux": "^5.0.2", 34 | "react-redux-typescript": "^2.3.0", 35 | "redux": "^3.6.0", 36 | "redux-persist": "^4.1.0", 37 | "redux-saga": "^0.15.6", 38 | "seamless-immutable": "^7.0.1", 39 | "typesafe-actions": "1.1.2" 40 | }, 41 | "devDependencies": { 42 | "@storybook/addon-storyshots": "^3.2.3", 43 | "@storybook/react-native": "^3.2.3", 44 | "@types/enzyme": "^3.1.0", 45 | "@types/ramda": "^0.24.17", 46 | "@types/react-navigation": "^1.0.21", 47 | "@types/react-redux": "^5.0.10", 48 | "@types/redux": "^3.6.0", 49 | "@types/jest": "^21.1.4", 50 | "@types/react": "^16.0.13", 51 | "@types/react-native": "^0.49.2", 52 | "@types/react-test-renderer": "^16.0.0", 53 | "@types/seamless-immutable": "^7.1.1", 54 | "@types/storybook__react": "^3.0.5", 55 | "@types/webpack-env": "^1.13.2", 56 | "babel-jest": "21.2.0", 57 | "babel-plugin-ignite-ignore-reactotron": "^0.3.0", 58 | "babel-preset-es2015": "^6.18.0", 59 | "babel-preset-react-native": "3.0.2", 60 | "enzyme": "^2.6.0", 61 | "jest-preset-typescript-react-native": "^1.2.0", 62 | "husky": "^0.13.1", 63 | "ignite-animatable": "^1.0.0", 64 | "ignite-dev-screens": "^2.2.0", 65 | "ignite-vector-icons": "^1.1.0", 66 | "jest": "^21.2.1", 67 | "mockery": "^2.0.0", 68 | "react-addons-test-utils": "~15.4.1", 69 | "react-dom": "16.0.0-alpha.12", 70 | "react-test-renderer": "16.0.0-beta.5", 71 | "reactotron-react-native": "^1.12.0", 72 | "reactotron-redux": "^1.11.1", 73 | "reactotron-redux-saga": "^1.11.1", 74 | "snazzy": "^7.0.0", 75 | "ts-jest": "^21.1.3", 76 | "tslint": "^5.7.0", 77 | "tslint-react": "^3.2.0", 78 | "typescript": "^2.5.3", 79 | "react-native-typescript-transformer": "^1.1.4" 80 | }, 81 | "jest": { 82 | "preset": "jest-preset-typescript-react-native", 83 | "testMatch": [ 84 | "**/Tests/**/*.ts?(x)", 85 | "**/App/**/*Test.ts?(x)" 86 | ], 87 | "testPathIgnorePatterns": [ 88 | "\\.snap$", 89 | "/node_modules/", 90 | "/lib/", 91 | "Tests/Setup" 92 | ], 93 | "setupFiles": [ 94 | "./Tests/Setup.tsx" 95 | ], 96 | "moduleFileExtensions": [ 97 | "js", 98 | "jsx", 99 | "ts", 100 | "tsx", 101 | "json" 102 | ], 103 | "cacheDirectory": ".jest/cache" 104 | }, 105 | "config": {} 106 | } 107 | -------------------------------------------------------------------------------- /commands/list.js: -------------------------------------------------------------------------------- 1 | // @cliDescription Generates a screen with a ListView/Flatlist/SectionList + walkthrough. 2 | 3 | const patterns = require('../lib/patterns') 4 | 5 | module.exports = async function (context) { 6 | // grab some features 7 | const { print, parameters, strings, ignite, filesystem } = context 8 | const { pascalCase, isBlank } = strings 9 | const config = ignite.loadIgniteConfig() 10 | 11 | // validation 12 | if (isBlank(parameters.first)) { 13 | print.info(`${context.runtime.brand} generate list \n`) 14 | print.info('A name is required.') 15 | return 16 | } 17 | 18 | const name = pascalCase(parameters.first) 19 | const props = { name } 20 | 21 | 22 | // which type of layout? 23 | const typeMessage = 'What kind of List would you like to generate?' 24 | const typeChoices = ['Row', 'Grid'] 25 | 26 | // Sections or no? 27 | const typeDataMessage = 'How will your data be presented on this list?' 28 | const typeDataChoices = ['Single', 'Sectioned'] 29 | 30 | // Check for parameters to bypass questions 31 | let typeCode = parameters.options.codeType 32 | let type = parameters.options.type 33 | let dataType = parameters.options.dataType 34 | 35 | // only prompt if type is not defined 36 | if (!typeCode) { 37 | typeCode = 'flatlist'; 38 | } 39 | 40 | if (!type) { 41 | // ask question 2 42 | const answers = await context.prompt.ask({ 43 | name: 'type', 44 | type: 'list', 45 | message: typeMessage, 46 | choices: typeChoices 47 | }) 48 | type = answers.type 49 | } 50 | 51 | if (!dataType) { 52 | // ask question 3 53 | const dataAnswers = await context.prompt.ask({ 54 | name: 'type', 55 | type: 'list', 56 | message: typeDataMessage, 57 | choices: typeDataChoices 58 | }) 59 | dataType = dataAnswers.type 60 | } 61 | 62 | // Sorry the following is so confusing, but so are React Native lists 63 | // There are 3 options and therefore 8 possible combinations 64 | let componentTemplate = dataType.toLowerCase() === 'sectioned' 65 | ? typeCode + '-sections' 66 | : typeCode 67 | let styleTemplate = '' 68 | // Different logic depending on code types 69 | if (typeCode === 'flatlist') { 70 | /* 71 | * The following mess is because FlatList supports numColumns 72 | * where SectionList does not. 73 | */ 74 | if (type.toLowerCase() === 'grid' && dataType.toLowerCase() === 'sectioned') { 75 | // grid + section means we need wrap 76 | styleTemplate = 'listview-grid-style' 77 | } else if (type.toLowerCase() === 'grid') { 78 | componentTemplate = componentTemplate + '-grid' 79 | // grid + single = no wrap, use columns 80 | styleTemplate = 'flatlist-grid-style' 81 | } else { 82 | // no grids, flatlist basic 83 | styleTemplate = 'listview-style' 84 | } 85 | } else { 86 | // listview builder 87 | styleTemplate = type.toLowerCase() === 'grid' 88 | ? 'listview-grid-style' 89 | : 'listview-style' 90 | } 91 | 92 | const jobs = [ 93 | { 94 | template: `${componentTemplate}.ejs`, 95 | target: `App/Containers/${name}/${name}.tsx` 96 | }, 97 | { 98 | template: `${styleTemplate}.ejs`, 99 | target: `App/Containers/${name}/${name}Style.ts` 100 | } 101 | ] 102 | 103 | await ignite.copyBatch(context, jobs, props) 104 | 105 | // if using `react-navigation` go the extra step 106 | // and insert the screen into the nav router 107 | if (config.navigation === 'react-navigation') { 108 | const screenName = `${name}` 109 | const appNavFilePath = `${process.cwd()}/App/Navigation/AppNavigation.tsx` 110 | const importToAdd = `import { ${screenName} } from "../Containers/${screenName}";` 111 | const routeToAdd = ` ${screenName}: { screen: ${screenName} },` 112 | 113 | if (!filesystem.exists(appNavFilePath)) { 114 | const msg = `No '${appNavFilePath}' file found. Can't insert list screen.` 115 | print.error(msg) 116 | process.exit(1) 117 | } 118 | 119 | // insert list screen import 120 | ignite.patchInFile(appNavFilePath, { 121 | after: patterns[patterns.constants.PATTERN_IMPORTS], 122 | insert: importToAdd 123 | }) 124 | 125 | // insert list screen route 126 | ignite.patchInFile(appNavFilePath, { 127 | after: patterns[patterns.constants.PATTERN_ROUTES], 128 | insert: routeToAdd 129 | }) 130 | } else { 131 | print.info('List screen created, manually add it to your navigation') 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /boilerplate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | //"outDir": "./dist", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 27 | 28 | /* Additional Checks */ 29 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 33 | 34 | /* Module Resolution Options */ 35 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 36 | "baseUrl": "types", /* Base directory to resolve non-absolute module names. */ 37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 39 | "typeRoots": ["types"], /* List of folders to include type definitions from. */ 40 | // "types": [], /* Type declaration files to be included in compilation. */ 41 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 42 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 43 | 44 | /* Source Map Options */ 45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 49 | 50 | /* Experimental Options */ 51 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 52 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 53 | } 54 | // "include": [ 55 | // "index.ts", "App/Components/Stories.tsx" 56 | // ] 57 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ESNEXT", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 5 | "module": "ESNext", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation: */ 7 | "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "./", /* Concatenate and emit output to single file. */ 13 | "outDir": "./dist", /* Redirect output structure to the directory. */ 14 | // "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | "strict": true, /* Enable all strict type-checking options. */ 23 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 24 | // "strictNullChecks": true, /* Enable strict null checks. */ 25 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 26 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 27 | 28 | /* Additional Checks */ 29 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 33 | 34 | /* Module Resolution Options */ 35 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 36 | "baseUrl": "./boilerplate/", /* Base directory to resolve non-absolute module names. */ 37 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 38 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 39 | "typeRoots": ["./boilerplate/types/", "./node_modules/"], /* List of folders to include type definitions from. */ 40 | // "types": [], /* Type declaration files to be included in compilation. */ 41 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 42 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 43 | 44 | /* Source Map Options */ 45 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 46 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 47 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 48 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 49 | 50 | /* Experimental Options */ 51 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 52 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 53 | } 54 | } -------------------------------------------------------------------------------- /templates/flatlist-sections.ejs: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { View, SectionList, Text } from "react-native"; 3 | 4 | // More info here: https://facebook.github.io/react-native/docs/sectionlist.html 5 | 6 | // Styles 7 | import styles from "./<%= props.name %>Style"; 8 | 9 | export interface Section { 10 | key: string; 11 | data: RowItem[]; 12 | } 13 | export interface RowItem { 14 | title: string; 15 | description: string; 16 | } 17 | interface Props { 18 | data: Section[]; 19 | } 20 | 21 | class <%= props.name %> extends React.PureComponent { 22 | 23 | 24 | /* *********************************************************** 25 | * `renderItem` function - How each cell should be rendered 26 | * It's our best practice to place a single component here: 27 | * 28 | * e.g. 29 | * return 30 | * 31 | * For sections with different cells (heterogeneous lists), you can do branch 32 | * logic here based on section.key OR at the data level, you can provide 33 | * `renderItem` functions in each section. 34 | * 35 | * Note: You can remove section/separator functions and jam them in here 36 | *************************************************************/ 37 | renderItem ({section, item}: {section: Section, item: RowItem}) { 38 | return ( 39 | 40 | Section {section.key} - {item.title} 41 | {item.description} 42 | 43 | ) 44 | } 45 | 46 | // Conditional branching for section headers, also see step 3 47 | renderSectionHeader ({section}: {section: Section}) { 48 | switch (section.key) { 49 | case "First": 50 | return First Section 51 | default: 52 | return Second Section 53 | } 54 | } 55 | 56 | /* *********************************************************** 57 | * Consider the configurations we've set below. Customize them 58 | * to your liking! Each with some friendly advice. 59 | * 60 | * Removing a function here will make SectionList use default 61 | *************************************************************/ 62 | // Render a header? 63 | renderHeader = () => 64 | - Full List Header - 65 | 66 | // Render a footer? 67 | renderFooter = () => 68 | - Full List Footer - 69 | 70 | // Does each section need a footer? 71 | renderSectionFooter = () => 72 | END SECTION!!!! 73 | 74 | // Show this when data is empty 75 | renderEmpty = () => 76 | - Nothing to See Here - 77 | 78 | renderSeparator = () => 79 | - ~~~~~ - 80 | 81 | renderSectionSeparator = () => 82 | \/\/\/\/\/\/\/\/ 83 | 84 | // The default function if no Key is provided is index 85 | // an identifiable key is important if you plan on 86 | // item reordering. Otherwise index is fine 87 | keyExtractor = (item, index) => index 88 | 89 | // How many items should be kept im memory as we scroll? 90 | oneScreensWorth = 20 91 | 92 | // extraData is for anything that is not indicated in data 93 | // for instance, if you kept "favorites" in `this.state.favs` 94 | // pass that in, so changes in favorites will cause a re-render 95 | // and your renderItem will have access to change depending on state 96 | // e.g. `extraData`={this.state.favs} 97 | 98 | // Optimize your list if the height of each item can be calculated 99 | // by supplying a constant height, there is no need to measure each 100 | // item after it renders. This can save significant time for lists 101 | // of a size 100+ 102 | // e.g. itemLayout={(data, index) => ( 103 | // {length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index} 104 | // )} 105 | 106 | render () { 107 | return ( 108 | 109 | 124 | 125 | ) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /test/generators-integration.test.js: -------------------------------------------------------------------------------- 1 | const execa = require('execa') 2 | const jetpack = require('fs-jetpack') 3 | const tempy = require('tempy') 4 | 5 | const IGNITE = 'ignite' 6 | const APP = 'IntegrationTest' 7 | const BOILERPLATE = `${__dirname}/..` 8 | console.warn(BOILERPLATE) 9 | // calling the ignite cli takes a while 10 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000 11 | 12 | describe('without a linter', () => { 13 | beforeAll(async () => { 14 | // creates a new temp directory 15 | process.chdir(tempy.directory()) 16 | await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--no-lint', '--boilerplate', BOILERPLATE]) 17 | process.chdir(APP) 18 | }) 19 | 20 | test('does not have a linting script', async () => { 21 | expect(jetpack.read('package.json', 'json')['scripts']['lint']).toBe(undefined) 22 | }) 23 | }) 24 | 25 | describe('generators', () => { 26 | beforeAll(async () => { 27 | // creates a new temp directory 28 | process.chdir(tempy.directory()) 29 | await execa(IGNITE, ['new', APP, '--min', '--skip-git', '--boilerplate', BOILERPLATE]) 30 | process.chdir(APP) 31 | }) 32 | 33 | test('generates a component', async () => { 34 | await execa(IGNITE, ['g', 'component', 'Test'], { preferLocal: false }) 35 | expect(jetpack.exists('App/Components/Test.tsx')).toBe('file') 36 | expect(jetpack.exists('App/Components/Styles/TestStyle.ts')).toBe('file') 37 | const lint = await execa('npm', ['-s', 'run', 'lint']) 38 | expect(lint.stderr).toBe('') 39 | }) 40 | 41 | test('generate listview of type row works', async () => { 42 | await execa(IGNITE, ['g', 'list', 'TestRow', '--type=Row', '--codeType=listview', '--dataType=Single'], { preferLocal: false }) 43 | expect(jetpack.exists('App/Containers/TestRow.tsx')).toBe('file') 44 | expect(jetpack.exists('App/Containers/Styles/TestRowStyle.ts')).toBe('file') 45 | const lint = await execa('npm', ['run', 'lint']) 46 | expect(lint.stderr).toBe('') 47 | }) 48 | 49 | test('generate flatlist of type row works', async () => { 50 | await execa(IGNITE, ['g', 'list', 'TestFlatRow', '--type=Row', '--codeType=flatlist', '--dataType=Single'], { preferLocal: false }) 51 | expect(jetpack.exists('App/Containers/TestFlatRow.tsx')).toBe('file') 52 | expect(jetpack.exists('App/Containers/Styles/TestFlatRowStyle.ts')).toBe('file') 53 | const lint = await execa('npm', ['run', 'lint']) 54 | expect(lint.stderr).toBe('') 55 | }) 56 | 57 | test('generate listview of sections works', async () => { 58 | await execa(IGNITE, ['g', 'list', 'TestSection', '--type=Row', '--codeType=listview', '--dataType=Sectioned'], { preferLocal: false }) 59 | expect(jetpack.exists('App/Containers/TestSection.tsx')).toBe('file') 60 | expect(jetpack.exists('App/Containers/Styles/TestSectionStyle.ts')).toBe('file') 61 | const lint = await execa('npm', ['run', 'lint']) 62 | expect(lint.stderr).toBe('') 63 | }) 64 | 65 | test('generate flatlist of sections works', async () => { 66 | await execa(IGNITE, ['g', 'list', 'TestFlatSection', '--type=Row', '--codeType=flatlist', '--dataType=Sectioned'], { preferLocal: false }) 67 | expect(jetpack.exists('App/Containers/TestFlatSection.tsx')).toBe('file') 68 | expect(jetpack.exists('App/Containers/Styles/TestFlatSectionStyle.ts')).toBe('file') 69 | const lint = await execa('npm', ['run', 'lint']) 70 | expect(lint.stderr).toBe('') 71 | }) 72 | 73 | test('generate listview of type grid works', async () => { 74 | await execa(IGNITE, ['g', 'list', 'TestGrid', '--type=Grid', '--codeType=listview', '--dataType=Single'], { preferLocal: false }) 75 | expect(jetpack.exists('App/Containers/TestGrid.tsx')).toBe('file') 76 | expect(jetpack.exists('App/Containers/Styles/TestGridStyle.ts')).toBe('file') 77 | const lint = await execa('npm', ['run', 'lint']) 78 | expect(lint.stderr).toBe('') 79 | }) 80 | 81 | test('generate redux works', async () => { 82 | await execa(IGNITE, ['g', 'redux', 'Test'], { preferLocal: false }) 83 | expect(jetpack.exists('App/Redux/TestRedux.tsx')).toBe('file') 84 | const lint = await execa('npm', ['run', 'lint']) 85 | expect(lint.stderr).toBe('') 86 | }) 87 | 88 | test('generate container works', async () => { 89 | await execa(IGNITE, ['g', 'container', 'Container'], { preferLocal: false }) 90 | expect(jetpack.exists('App/Containers/Container.tsx')).toBe('file') 91 | expect(jetpack.exists('App/Containers/Styles/ContainerStyle.ts')).toBe('file') 92 | const lint = await execa('npm', ['run', 'lint']) 93 | expect(lint.stderr).toBe('') 94 | }) 95 | 96 | test('generate saga works', async () => { 97 | await execa(IGNITE, ['g', 'saga', 'Test'], { preferLocal: false }) 98 | expect(jetpack.exists('App/Sagas/TestSagas.tsx')).toBe('file') 99 | const lint = await execa('npm', ['run', 'lint']) 100 | expect(lint.stderr).toBe('') 101 | }) 102 | 103 | test('generate screen works', async () => { 104 | await execa(IGNITE, ['g', 'screen', 'Test'], { preferLocal: false }) 105 | expect(jetpack.exists('App/Containers/TestScreen.tsx')).toBe('file') 106 | expect(jetpack.exists('App/Containers/Styles/TestScreenStyle.ts')).toBe('file') 107 | const lint = await execa('npm', ['run', 'lint']) 108 | expect(lint.stderr).toBe('') 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /boilerplate/App/Fixtures/gantman.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 7, 3 | "incomplete_results": false, 4 | "items": [ 5 | { 6 | "login": "GantMan", 7 | "id": 997157, 8 | "avatar_url": "https://avatars.githubusercontent.com/u/997157?v=3", 9 | "gravatar_id": "", 10 | "url": "https://api.github.com/users/GantMan", 11 | "html_url": "https://github.com/GantMan", 12 | "followers_url": "https://api.github.com/users/GantMan/followers", 13 | "following_url": "https://api.github.com/users/GantMan/following{/other_user}", 14 | "gists_url": "https://api.github.com/users/GantMan/gists{/gist_id}", 15 | "starred_url": "https://api.github.com/users/GantMan/starred{/owner}{/repo}", 16 | "subscriptions_url": "https://api.github.com/users/GantMan/subscriptions", 17 | "organizations_url": "https://api.github.com/users/GantMan/orgs", 18 | "repos_url": "https://api.github.com/users/GantMan/repos", 19 | "events_url": "https://api.github.com/users/GantMan/events{/privacy}", 20 | "received_events_url": "https://api.github.com/users/GantMan/received_events", 21 | "type": "User", 22 | "site_admin": false, 23 | "score": 122.12115 24 | }, 25 | { 26 | "login": "vlad-G", 27 | "id": 13520880, 28 | "avatar_url": "https://avatars.githubusercontent.com/u/13520880?v=3", 29 | "gravatar_id": "", 30 | "url": "https://api.github.com/users/vlad-G", 31 | "html_url": "https://github.com/vlad-G", 32 | "followers_url": "https://api.github.com/users/vlad-G/followers", 33 | "following_url": "https://api.github.com/users/vlad-G/following{/other_user}", 34 | "gists_url": "https://api.github.com/users/vlad-G/gists{/gist_id}", 35 | "starred_url": "https://api.github.com/users/vlad-G/starred{/owner}{/repo}", 36 | "subscriptions_url": "https://api.github.com/users/vlad-G/subscriptions", 37 | "organizations_url": "https://api.github.com/users/vlad-G/orgs", 38 | "repos_url": "https://api.github.com/users/vlad-G/repos", 39 | "events_url": "https://api.github.com/users/vlad-G/events{/privacy}", 40 | "received_events_url": "https://api.github.com/users/vlad-G/received_events", 41 | "type": "User", 42 | "site_admin": false, 43 | "score": 12.69848 44 | }, 45 | { 46 | "login": "gantmani", 47 | "id": 3034094, 48 | "avatar_url": "https://avatars.githubusercontent.com/u/3034094?v=3", 49 | "gravatar_id": "", 50 | "url": "https://api.github.com/users/gantmani", 51 | "html_url": "https://github.com/gantmani", 52 | "followers_url": "https://api.github.com/users/gantmani/followers", 53 | "following_url": "https://api.github.com/users/gantmani/following{/other_user}", 54 | "gists_url": "https://api.github.com/users/gantmani/gists{/gist_id}", 55 | "starred_url": "https://api.github.com/users/gantmani/starred{/owner}{/repo}", 56 | "subscriptions_url": "https://api.github.com/users/gantmani/subscriptions", 57 | "organizations_url": "https://api.github.com/users/gantmani/orgs", 58 | "repos_url": "https://api.github.com/users/gantmani/repos", 59 | "events_url": "https://api.github.com/users/gantmani/events{/privacy}", 60 | "received_events_url": "https://api.github.com/users/gantmani/received_events", 61 | "type": "User", 62 | "site_admin": false, 63 | "score": 11.641713 64 | }, 65 | { 66 | "login": "sgantman", 67 | "id": 5911526, 68 | "avatar_url": "https://avatars.githubusercontent.com/u/5911526?v=3", 69 | "gravatar_id": "", 70 | "url": "https://api.github.com/users/sgantman", 71 | "html_url": "https://github.com/sgantman", 72 | "followers_url": "https://api.github.com/users/sgantman/followers", 73 | "following_url": "https://api.github.com/users/sgantman/following{/other_user}", 74 | "gists_url": "https://api.github.com/users/sgantman/gists{/gist_id}", 75 | "starred_url": "https://api.github.com/users/sgantman/starred{/owner}{/repo}", 76 | "subscriptions_url": "https://api.github.com/users/sgantman/subscriptions", 77 | "organizations_url": "https://api.github.com/users/sgantman/orgs", 78 | "repos_url": "https://api.github.com/users/sgantman/repos", 79 | "events_url": "https://api.github.com/users/sgantman/events{/privacy}", 80 | "received_events_url": "https://api.github.com/users/sgantman/received_events", 81 | "type": "User", 82 | "site_admin": false, 83 | "score": 7.926345 84 | }, 85 | { 86 | "login": "michaelgantman", 87 | "id": 16693070, 88 | "avatar_url": "https://avatars.githubusercontent.com/u/16693070?v=3", 89 | "gravatar_id": "", 90 | "url": "https://api.github.com/users/michaelgantman", 91 | "html_url": "https://github.com/michaelgantman", 92 | "followers_url": "https://api.github.com/users/michaelgantman/followers", 93 | "following_url": "https://api.github.com/users/michaelgantman/following{/other_user}", 94 | "gists_url": "https://api.github.com/users/michaelgantman/gists{/gist_id}", 95 | "starred_url": "https://api.github.com/users/michaelgantman/starred{/owner}{/repo}", 96 | "subscriptions_url": "https://api.github.com/users/michaelgantman/subscriptions", 97 | "organizations_url": "https://api.github.com/users/michaelgantman/orgs", 98 | "repos_url": "https://api.github.com/users/michaelgantman/repos", 99 | "events_url": "https://api.github.com/users/michaelgantman/events{/privacy}", 100 | "received_events_url": "https://api.github.com/users/michaelgantman/received_events", 101 | "type": "User", 102 | "site_admin": false, 103 | "score": 7.926345 104 | }, 105 | { 106 | "login": "gantmanis", 107 | "id": 19141249, 108 | "avatar_url": "https://avatars.githubusercontent.com/u/19141249?v=3", 109 | "gravatar_id": "", 110 | "url": "https://api.github.com/users/gantmanis", 111 | "html_url": "https://github.com/gantmanis", 112 | "followers_url": "https://api.github.com/users/gantmanis/followers", 113 | "following_url": "https://api.github.com/users/gantmanis/following{/other_user}", 114 | "gists_url": "https://api.github.com/users/gantmanis/gists{/gist_id}", 115 | "starred_url": "https://api.github.com/users/gantmanis/starred{/owner}{/repo}", 116 | "subscriptions_url": "https://api.github.com/users/gantmanis/subscriptions", 117 | "organizations_url": "https://api.github.com/users/gantmanis/orgs", 118 | "repos_url": "https://api.github.com/users/gantmanis/repos", 119 | "events_url": "https://api.github.com/users/gantmanis/events{/privacy}", 120 | "received_events_url": "https://api.github.com/users/gantmanis/received_events", 121 | "type": "User", 122 | "site_admin": false, 123 | "score": 7.8813524 124 | }, 125 | { 126 | "login": "Gantman2014", 127 | "id": 7669410, 128 | "avatar_url": "https://avatars.githubusercontent.com/u/7669410?v=3", 129 | "gravatar_id": "", 130 | "url": "https://api.github.com/users/Gantman2014", 131 | "html_url": "https://github.com/Gantman2014", 132 | "followers_url": "https://api.github.com/users/Gantman2014/followers", 133 | "following_url": "https://api.github.com/users/Gantman2014/following{/other_user}", 134 | "gists_url": "https://api.github.com/users/Gantman2014/gists{/gist_id}", 135 | "starred_url": "https://api.github.com/users/Gantman2014/starred{/owner}{/repo}", 136 | "subscriptions_url": "https://api.github.com/users/Gantman2014/subscriptions", 137 | "organizations_url": "https://api.github.com/users/Gantman2014/orgs", 138 | "repos_url": "https://api.github.com/users/Gantman2014/repos", 139 | "events_url": "https://api.github.com/users/Gantman2014/events{/privacy}", 140 | "received_events_url": "https://api.github.com/users/Gantman2014/received_events", 141 | "type": "User", 142 | "site_admin": false, 143 | "score": 7.8813524 144 | } 145 | ] 146 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Ignite TypeScript Boilerplate for React Native 2 | 3 | ### The easiest way to develop React Native apps in TypeScript. 4 | Get up and running with TypeScript React Native development in minutes. A batteries-included, opinionated starter project, and code generators for your components, reducers, sagas and more. 5 | Originally based on a port of the [Ignite IR Boilerplate](https://github.com/infinitered/ignite-ir-boilerplate) to TypeScript. 6 | 7 | Currently includes: 8 | 9 | * React Native 0.51.0 (but you can change this if you want to experiment) 10 | * React Navigation 11 | * Redux 12 | * Redux Sagas 13 | * And more! 14 | 15 | ## Quick Start 16 | 17 | When you've installed the [Ignite CLI](https://github.com/infinitered/ignite), (tl;dr: `npm install -g ignite-cli`) you can get started with this boilerplate like this: 18 | 19 | ```sh 20 | ignite new MyLatestCreation --b ignite-typescript-boilerplate 21 | ``` 22 | 23 | You can also change the React Native version, just keep in mind, we may not have tested this just yet. 24 | 25 | ```sh 26 | ignite new MyLatestCreation --b ignite-typescript-boilerplate --react-native-version 0.46.0-rc.2 27 | ``` 28 | 29 | 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: 30 | 31 | ```sh 32 | ignite new MyLatestCreation --b ignite-typescript-boilerplate --max 33 | ``` 34 | 35 | If you want very few of these extras: 36 | 37 | ```sh 38 | ignite new MyLatestCreation --b ignite-typescript-boilerplate --min 39 | ``` 40 | 41 | ## Using TypeScript with React Native 42 | 43 | Thanks to the beauty of [react-native-typescript-transformer](https://github.com/ds300/react-native-typescript-transformer), we can seamlessly use TypeScript in our React Native project. Source maps and hot reloading all work just like you would expect. 44 | 45 | ## Coding style 46 | 47 | We use `tslint` to enforce coding style, with rules based on [Palantir's tslint-react](https://github.com/palantir/tslint-react), 48 | and a few changes to accommodate some Ignite quirks. If you install a plugin, your editor can probably automatically fix problems. 49 | In VS Code, set `"tslint.autoFixOnSave": true` in your 50 | workspace settings. You can run the linter from the command line. `npm run lint` runs the linter, while `npm run fixcode` tries to autofix problems. 51 | 52 | ## Boilerplate walkthrough 53 | 54 | Your `App` folder is where most of the goodies are found in an Ignite app. Let's walk through them in more detail. Start with `Containers/App.tsx` (described below) and work your way down the walkthrough in order. 55 | 56 | ### Components 57 | 58 | React components go here. We generate these as stateless functional components by default, as recommended by the React team. 59 | 60 | ### Containers 61 | 62 | Containers are Redux-connected components, and are mostly full screens. 63 | 64 | * `App.tsx` - your main application. We create a Redux store and configure it here 65 | * `RootContainer.tsx` - main view of your application. Contains your status bar and navigation component 66 | * `LaunchScreen.tsx` - this is the first screen shown in your application. It's loaded into the Navigation component 67 | 68 | ### Navigation 69 | 70 | Your primary and other navigation components reside here. 71 | 72 | * `AppNavigation.tsx` - loads in your initial screen and creates your menu(s) in a StackNavigation 73 | * `Styles` - styling for the navigation 74 | 75 | ### Storybook 76 | 77 | [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 `yarn run storybook` to get started. All stories are contained in the `*.story.tsx` files along side the components. 78 | 79 | ### Themes 80 | 81 | Styling themes used throughout your app styles. 82 | 83 | * `ApplicationStyles.ts` - app-wide styles 84 | * `Colors.ts` - defined colors for your app 85 | * `Fonts.ts` - defined fonts for your app 86 | * `Images.ts` - loads and caches images used in your app 87 | * `Metrics.ts` - useful measurements of things like navBarHeight 88 | 89 | ### Config 90 | 91 | Initialize and configure things here. 92 | 93 | * `AppConfig.ts` - simple React Native configuration here 94 | * `DebugConfig.js` - define how you want your debug environment to act. This is a .js file because that's what 95 | Ignite expects to find. 96 | * `ReactotronConfig.ts` - 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) 97 | 98 | ### Fixtures 99 | 100 | Contains json files that mimic API responses for quicker development. These are used by the `Services/FixtureApi.ts` object to mock API responses. 101 | 102 | ### Redux, Sagas 103 | 104 | Contains a preconfigured Redux and Redux-Sagas setup. Review each file carefully to see how Redux interacts with your application. You will find these in the Reducers and Sagas folders. We use [typesafe-actions](https://github.com/piotrwitek/typesafe-actions) to get lovely 105 | type checking of our reducers and actions. Take a look at `Lib/ReduxHelpers.ts` for some extra functions that 106 | we use to make them more Ignite-y. 107 | 108 | ### Services 109 | 110 | Contains your API service and other important utilities for your application. 111 | 112 | * `Api.tsx` - main API service, giving you an interface to communicate with your back end 113 | * `ExamplesRegistry.tsx` - lets you view component and Ignite plugin examples in your app 114 | * `FixtureApi.tsx` - mocks your API service, making it faster to develop early on in your app 115 | 116 | 117 | ### Lib 118 | 119 | We recommend using this folder for modules that can be extracted into their own NPM packages at some point. 120 | 121 | ### Images 122 | 123 | Contains actual images (usually png) used in your application. 124 | 125 | ### Transforms 126 | 127 | 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. 128 | 129 | ### Tests 130 | 131 | We create Jest tests alongside the components, reducers and sagas. Enable this by adding `"tests": "jest"` to `ignite/ignite.json`. 132 | 133 | ### Code generation 134 | 135 | Currently, the following code generation commands work properly: 136 | * `ignite generate component MyComponent` - generates a stateless functional component. 137 | * `ignite generate container MyContainer` - generates a Redux-connected React.Component, with state and view lifecycle. 138 | * `ignite generate screen MyScreen` - generates a Redux-connected React.Component, with state, view lifecycle and react-navigation. 139 | * `ignite generate reducers MyNew` - generates a set of Redux reducers. 140 | * `ignite generate saga MySaga` - generates a Redux Saga 141 | * `ignite generate list MyList` - generates a FlatList, formatted either as a grid or list. 142 | 143 | ### Further reading 144 | 145 | A comprehensive guide to best practice with TypeScript in React is [the React Redux TypeScript Guide](https://github.com/piotrwitek/react-redux-typescript-guide), which covers a lot more than just Redux. We have adopted a lot of the patterns from this. The `typesafe-actions` library that we use was created by @piotrwitek, the author of the guide. 146 | 147 | Microsoft created [TypeScript React Native Starter](https://github.com/Microsoft/TypeScript-React-Native-Starter), which includes a walkthrough on switching projects to TypeScript. 148 | 149 | [React TypeScript Tutorial](https://github.com/DanielRosenwasser/React-TypeScript-Tutorial) is React rather than React Native, but has useful guides. 150 | 151 | [This post](http://blog.novanet.no/easy-typescript-with-react-native/) is a good run-through of the [react-native-typescript-transfomer](https://github.com/ds300/react-native-typescript-transformer), which allows us to skip the transpile step that we were using before. Thanks [@wormyy] for the heads-up on this. 152 | 153 | ### Credits 154 | Created by [Matt Kane](https://github.com/ascorbic) at [Aerian Studios](https://www.aerian.com). Based on [Ignite IR Boilerplate](https://github.com/infinitered/ignite-ir-boilerplate), by Infinite Red. -------------------------------------------------------------------------------- /boilerplate.js: -------------------------------------------------------------------------------- 1 | const options = require('./options') 2 | const { merge, pipe, assoc, omit, __ } = require('ramda') 3 | const { getReactNativeVersion } = require('./lib/react-native-version') 4 | 5 | /** 6 | * Is Android installed? 7 | * 8 | * $ANDROID_HOME/tools folder has to exist. 9 | * 10 | * @param {*} context - The gluegun context. 11 | * @returns {boolean} 12 | */ 13 | const isAndroidInstalled = function (context) { 14 | const androidHome = process.env['ANDROID_HOME'] 15 | const hasAndroidEnv = !context.strings.isBlank(androidHome) 16 | const hasAndroid = hasAndroidEnv && context.filesystem.exists(`${androidHome}/tools`) === 'dir' 17 | 18 | return Boolean(hasAndroid) 19 | } 20 | 21 | /** 22 | * Let's install. 23 | * 24 | * @param {any} context - The gluegun context. 25 | */ 26 | async function install (context) { 27 | const { 28 | filesystem, 29 | parameters, 30 | ignite, 31 | reactNative, 32 | print, 33 | system, 34 | prompt, 35 | template 36 | } = context 37 | const { colors } = print 38 | const { red, yellow, bold, gray, blue, green } = colors 39 | 40 | const perfStart = (new Date()).getTime() 41 | 42 | const name = parameters.third 43 | const logo = red(` 44 | 45 | 46 | -aeaeaeaeaeae— 47 | -eaeaeaeaeaeaeaeaeaeae- 48 | /aeaeaeaeaeaeaeaeaeaeaeaeae\\ 49 | /aeaeaeaeaeaeaeaeaeaeaeaeaeaeae\\ 50 | /eaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaea\\ 51 | /aeaeaeaeaeaeaeaeaeaea/ |aeaeaeaeaeae\\ 52 | /aaeaeaeaeaeaeaeaeaeae/ |aeaeaeaeaeaea\\ 53 | aeaeaeaeaeaeaeaeaeae/ |eaeaeaeaeaeaea 54 | |aeaeaeaeaeaeaeaeae/ |eaeaeaeaeaeaea| 55 | aeaeaeaeaeaeaeaea/ |eaeaeaeaeaeaeae 56 | eaeaeaeaeaeaeae/`) + yellow(`:`) + red(`\\ |aeaeaeaeaeaeaea 57 | aeaeaeaeaeaea/`) + yellow(`::::`) + red(`\\ |eaeaeaeaeaeaeae 58 | |aeaeaeaeae/`) + yellow(`:::::::`) + red(`\\ |eaeaeaeaeaeaea| 59 | aeaeaeaeaeaeaeaeaea\\ |`) + yellow(`::::`) + red(`/aeaeaeaea 60 | \\eaeaeaeaeaeaeaeaeaea\\ |`) + yellow(`:::`) + red(`/aeaeaeaea/ 61 | \\aeaeaeaeaeaeaeaeaeae\\ |`) + yellow(`::`) + red(`/aeaeaeaea/ 62 | \\aeaeaeaeaeaeaeaeaeae\\ |`) + yellow(`:`) + red(`/eaeeaeaea/ 63 | \\aeaeaeaeaeaeaeaeaea\\|/aeaeaeae/ 64 | \\aeaeaeaeaeaeaeaeaeaeaeaeae/ 65 | -eaeaeaeaeaeaeaeaeaeae- 66 | -aeaeaeaeaeae— 67 | 68 | __ _ ___ _ __ _ __ _ _ __ 69 | / _' |/ _ \\ '__| |/ _' | '_ \\ 70 | | (_| | __/ | | | (_| | | | | 71 | \\__,_|\\___|_| |_|\\__,_|_| |_| 72 | 73 | `) + green(` 74 | 🌳 Crafted with care in the Cotswolds. 🌳`) + yellow(` 75 | 76 | https://aerian.com/ 77 | 78 | `); 79 | 80 | print.info(logo) 81 | const spinner = print 82 | .spin(`using the TypeScript boilerplate from Aerian Studios. You might want to make a cuppa while we get this ready. ☕️`) 83 | .succeed() 84 | 85 | // attempt to install React Native or die trying 86 | const rnInstall = await reactNative.install({ 87 | name, 88 | version: getReactNativeVersion(context) 89 | }) 90 | if (rnInstall.exitCode > 0) process.exit(rnInstall.exitCode) 91 | 92 | // remove the __tests__ directory and App.js that come with React Native 93 | filesystem.remove('__tests__') 94 | filesystem.remove('App.js') 95 | // copy our App, Tests & storybook directories 96 | spinner.text = '▸ copying files' 97 | spinner.start() 98 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/App`, `${process.cwd()}/App`, { 99 | overwrite: true, 100 | matching: '!*.ejs' 101 | }) 102 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/Tests`, `${process.cwd()}/Tests`, { 103 | overwrite: true, 104 | matching: '!*.ejs' 105 | }) 106 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/storybook`, `${process.cwd()}/storybook`, { 107 | overwrite: true, 108 | matching: '!*.ejs' 109 | }) 110 | filesystem.copy(`${ignite.ignitePluginPath()}/boilerplate/types`, `${process.cwd()}/types`, { 111 | overwrite: true, 112 | matching: '!*.ejs' 113 | }) 114 | spinner.stop() 115 | 116 | // --max, --min, interactive 117 | let answers 118 | if (parameters.options.max) { 119 | answers = options.answers.max 120 | } else if (parameters.options.min) { 121 | answers = options.answers.min 122 | } else { 123 | answers = await prompt.ask(options.questions) 124 | } 125 | 126 | // generate some templates 127 | spinner.text = '▸ generating files' 128 | const templates = [ 129 | { template: 'index.js.ejs', target: 'index.js' }, 130 | { template: 'README.md', target: 'README.md' }, 131 | { template: 'ignite.json.ejs', target: 'ignite/ignite.json' }, 132 | { template: '.editorconfig', target: '.editorconfig' }, 133 | { template: '.babelrc', target: '.babelrc' }, 134 | { template: 'tsconfig.json', target: 'tsconfig.json' }, 135 | { template: 'tslint.json', target: 'tslint.json' }, 136 | { template: 'rn-cli.config.js', target: 'rn-cli.config.js' }, 137 | { template: 'Tests/Setup.tsx.ejs', target: 'Tests/Setup.tsx' }, 138 | { template: 'storybook/storybook.ejs', target: 'storybook/storybook.js' }, 139 | { template: '.env.example', target: '.env.example' } 140 | ] 141 | const templateProps = { 142 | name, 143 | igniteVersion: ignite.version, 144 | reactNativeVersion: rnInstall.version, 145 | vectorIcons: answers['vector-icons'], 146 | animatable: answers['animatable'], 147 | i18n: answers['i18n'] 148 | } 149 | await ignite.copyBatch(context, templates, templateProps, { 150 | quiet: false, 151 | directory: `${ignite.ignitePluginPath()}/boilerplate` 152 | }) 153 | 154 | /** 155 | * Append to files 156 | */ 157 | // https://github.com/facebook/react-native/issues/12724 158 | filesystem.appendAsync('.gitattributes', '*.bat text eol=crlf') 159 | filesystem.append('.gitignore', '\n# Misc\n#') 160 | filesystem.append('.gitignore', '\n.env.example\n') 161 | filesystem.append('.gitignore', '.env\n') 162 | filesystem.append('.gitignore', 'dist\n') 163 | filesystem.append('.gitignore', '.jest\n') 164 | 165 | 166 | /** 167 | * Merge the package.json from our template into the one provided from react-native init. 168 | */ 169 | async function mergePackageJsons () { 170 | // transform our package.json in case we need to replace variables 171 | const rawJson = await template.generate({ 172 | directory: `${ignite.ignitePluginPath()}/boilerplate`, 173 | template: 'package.json.ejs', 174 | props: templateProps 175 | }) 176 | const newPackageJson = JSON.parse(rawJson) 177 | 178 | // read in the react-native created package.json 179 | const currentPackage = filesystem.read('package.json', 'json') 180 | 181 | // deep merge, lol 182 | const newPackage = pipe( 183 | assoc( 184 | 'dependencies', 185 | merge(currentPackage.dependencies, newPackageJson.dependencies) 186 | ), 187 | assoc( 188 | 'devDependencies', 189 | merge(currentPackage.devDependencies, newPackageJson.devDependencies) 190 | ), 191 | assoc('scripts', merge(currentPackage.scripts, newPackageJson.scripts)), 192 | merge( 193 | __, 194 | omit(['dependencies', 'devDependencies', 'scripts'], newPackageJson) 195 | ) 196 | )(currentPackage) 197 | 198 | // write this out 199 | filesystem.write('package.json', newPackage, { jsonIndent: 2 }) 200 | } 201 | await mergePackageJsons() 202 | 203 | spinner.stop() 204 | 205 | // react native link -- must use spawn & stdio: ignore or it hangs!! :( 206 | spinner.text = `▸ linking native libraries` 207 | spinner.start() 208 | await system.spawn('react-native link', { stdio: 'ignore' }) 209 | spinner.stop() 210 | 211 | // pass long the debug flag if we're running in that mode 212 | const debugFlag = parameters.options.debug ? '--debug' : '' 213 | 214 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 215 | // NOTE(steve): I'm re-adding this here because boilerplates now hold permanent files 216 | // -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 217 | try { 218 | // boilerplate adds itself to get plugin.js/generators etc 219 | // Could be directory, npm@version, or just npm name. Default to passed in values 220 | const boilerplate = parameters.options.b || parameters.options.boilerplate || 'ignite-typescript-boilerplate' 221 | 222 | await system.spawn(`ignite add ${boilerplate} ${debugFlag}`, { stdio: 'inherit' }) 223 | 224 | // now run install of Ignite Plugins 225 | if (answers['dev-screens'] === 'Yes') { 226 | await system.spawn(`ignite add dev-screens@"~>2.2.0" ${debugFlag}`, { 227 | stdio: 'inherit' 228 | }) 229 | } 230 | 231 | if (answers['vector-icons'] === 'react-native-vector-icons') { 232 | await system.spawn(`ignite add vector-icons@"~>1.0.0" ${debugFlag}`, { 233 | stdio: 'inherit' 234 | }) 235 | } 236 | 237 | if (answers['i18n'] === 'react-native-i18n') { 238 | await system.spawn(`ignite add i18n@"~>1.0.0" ${debugFlag}`, { stdio: 'inherit' }) 239 | } 240 | 241 | if (answers['animatable'] === 'react-native-animatable') { 242 | await system.spawn(`ignite add animatable@"~>1.0.0" ${debugFlag}`, { 243 | stdio: 'inherit' 244 | }) 245 | } 246 | 247 | } catch (e) { 248 | ignite.log(e) 249 | throw e 250 | } 251 | 252 | // git configuration 253 | const gitExists = await filesystem.exists('./.git') 254 | if (!gitExists && !parameters.options['skip-git'] && system.which('git')) { 255 | // initial git 256 | const spinner = print.spin('configuring git') 257 | 258 | // TODO: Make husky hooks optional 259 | const huskyCmd = '' // `&& node node_modules/husky/bin/install .` 260 | system.run(`git init . && git add . && git commit -m "Initial commit." ${huskyCmd}`) 261 | 262 | spinner.succeed(`configured git`) 263 | } 264 | 265 | const perfDuration = parseInt(((new Date()).getTime() - perfStart) / 10) / 100 266 | spinner.succeed(`ignited ${yellow(name)} in ${perfDuration}s`) 267 | 268 | const androidInfo = isAndroidInstalled(context) ? '' 269 | : `\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.` 270 | 271 | const successMessage = ` 272 | ${red('Ignite CLI')} ignited ${yellow(name)} in ${gray(`${perfDuration}s`)} 273 | 274 | To get started: 275 | 276 | cd ${name} 277 | react-native run-ios 278 | react-native run-android${androidInfo} 279 | ignite --help 280 | 281 | ${gray('Read the walkthrough at https://github.com/aerian-studios/ignite-typescript-boilerplate/blob/master/readme.md#boilerplate-walkthrough')} 282 | 283 | ${bold('Now get cooking! 🍽')} 284 | ` 285 | 286 | 287 | print.info(successMessage) 288 | } 289 | 290 | module.exports = { 291 | install 292 | } 293 | --------------------------------------------------------------------------------