├── .eslintignore
├── .eslintrc
├── .github
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .prettierrc
├── .ruby-version
├── .travis.yml
├── .vscode
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── _config.yml
├── analytics.js
├── assets
└── logos.png
├── generators
├── advanced-base
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ ├── babelrc
│ │ ├── index.js
│ │ ├── src
│ │ ├── App.js
│ │ ├── Scenes.js
│ │ ├── components
│ │ │ ├── Button.js
│ │ │ ├── ButtonCard.js
│ │ │ ├── Page.js
│ │ │ ├── ProfileHeader.js
│ │ │ ├── SecondaryFlatButton.js
│ │ │ ├── TextInput.js
│ │ │ ├── Touchable.js
│ │ │ ├── __tests__
│ │ │ │ ├── Button.test.js
│ │ │ │ └── __snapshots__
│ │ │ │ │ └── Button.test.js.snap
│ │ │ └── index.js
│ │ ├── lib
│ │ │ ├── i18n.js
│ │ │ └── package.json
│ │ ├── pages
│ │ │ ├── Account.js
│ │ │ ├── Home.js
│ │ │ ├── Landing.js
│ │ │ ├── Login.js
│ │ │ ├── Signup.js
│ │ │ └── index.js
│ │ └── theme
│ │ │ ├── images
│ │ │ ├── default-user-image.png
│ │ │ ├── landing.jpg
│ │ │ └── logo.png
│ │ │ └── index.js
│ │ └── translations
│ │ ├── en
│ │ └── translations.json
│ │ ├── fr
│ │ └── translations.json
│ │ └── index.js
├── app
│ └── index.js
├── assets
│ ├── README.md
│ ├── doc
│ │ ├── splashscreen-with-bottom-content.png
│ │ └── splashscreen-with-right-side-content.png
│ ├── getPixelColor.js
│ ├── imageGenerator.js
│ ├── index.js
│ └── templates
│ │ ├── android
│ │ ├── colors.xml
│ │ ├── launch_screen_bitmap.xml
│ │ └── styles.xml
│ │ └── ios
│ │ ├── AppIconsetContents.json
│ │ └── LaunchImageLaunchimageContents.json
├── base
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ ├── index.js
│ │ └── src
│ │ ├── App.js
│ │ ├── RootNavigation.js
│ │ ├── components
│ │ ├── Page.js
│ │ └── index.js
│ │ ├── modules
│ │ ├── App
│ │ │ ├── index.js
│ │ │ └── module.js
│ │ ├── Nav
│ │ │ ├── index.js
│ │ │ └── module.js
│ │ ├── rootEnhancer.js
│ │ ├── rootReducer.js
│ │ └── store.js
│ │ ├── pages
│ │ ├── Home
│ │ │ ├── Home.component.js
│ │ │ ├── Home.container.js
│ │ │ ├── Home.style.js
│ │ │ └── index.js
│ │ └── index.js
│ │ └── theme
│ │ └── index.js
├── bitrise
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ └── bitrise.yml
├── checkversion
│ ├── checkUpdate.js
│ └── index.js
├── circleci
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ └── config.yml
├── fastlane-env
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ ├── environment
│ │ └── index.js
│ │ └── fastlane
│ │ ├── env
│ │ └── env.secret
├── fastlane-setup
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ ├── Gemfile
│ │ ├── deploy.sh
│ │ ├── env
│ │ ├── environment
│ │ └── index.js
│ │ ├── fastlane
│ │ ├── Appfile
│ │ ├── Fastfile
│ │ └── ios_devices.txt
│ │ ├── gitignore
│ │ ├── gradleSigning
│ │ ├── secrets-scripts
│ │ ├── pack-secrets.sh
│ │ └── unpack-secrets.sh
│ │ └── strings.xml
├── jest
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ ├── Button.test.js
│ │ ├── FileStub.js
│ │ └── firstTest.js
├── lint
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ ├── eslintignore
│ │ ├── eslintrc
│ │ └── prettierrc
├── travisci
│ ├── README.md
│ ├── index.js
│ └── templates
│ │ └── travis.yml
└── vscode
│ ├── README.md
│ ├── index.js
│ └── templates
│ ├── jsconfig.json
│ └── settings.json
├── jsconfig.json
├── package.json
├── test.js
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | TestApp
2 | generators/**/templates
3 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "describe": true,
4 | "it": true
5 | },
6 | "extends": "universe/node",
7 | "rules": {
8 | "class-methods-use-this": 0,
9 | "key-spacing": 0,
10 | "no-underscore-dangle": 0,
11 | "no-console": 0
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Hi, thanks for submitting a Pull Request! :balloon:
2 |
3 | Before submitting, please be sure to check a few things:
4 |
5 | ## Commit names formatting
6 |
7 | This repository is automatically published to npm with
8 | [semantic-release](https://github.com/semantic-release/semantic-release).
9 |
10 | In order to manage that, your commit should follow the AngularJS commit
11 | conventions
12 | [as explained here](https://github.com/semantic-release/semantic-release#default-commit-message-format)
13 |
14 | You can use `yarn commit` to write a commit respecting this convention.
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | TestApp
3 | AdvancedTestApp
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | 2.3.1
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | branches:
5 | only:
6 | - master
7 | install:
8 | # Install React Native, Yarn and Yeoman:
9 | - npm install
10 | before_script:
11 | - npm prune
12 | after_success:
13 | - npm run semantic-release
14 | branches:
15 | except:
16 | - /^v\d+\.\d+\.\d+$/
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "Yeoman generator",
11 | "program": "/usr/local/bin/yo",
12 | "args": ["rn-toolbox:fastlane-env"],
13 | "console": "integratedTerminal",
14 | "internalConsoleOptions": "neverOpen",
15 | "cwd": "${workspaceFolder}/../yourProject"
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "prettier.singleQuote": true,
4 | "prettier.trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 BAM
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # React Native Toolbox [](https://travis-ci.org/bamlab/generator-rn-toolbox) [](https://www.npmjs.com/package/generator-rn-toolbox) []()
6 |
7 | Yeoman generators to kickstart your react-native v0.48+ projects.
8 |
9 | ## ⚠️ Deprecation notice ⚠️
10 |
11 | > **Dear user**, we are working on a complete rewrite of generator-rn-toolbox
12 | >
13 | > We are be deprecating this repo and migrating its features to [react-native-make](https://github.com/bamlab/react-native-make)
14 | >
15 | > Already available:
16 | >
17 | > - Updated Icons generation with [Android adaptive icons](https://medium.com/google-design/designing-adaptive-icons-515af294c783)
18 | > - Updated SplashScreen generation with iOS _.xib_ files and ready for [react-native-splashscreen](https://github.com/crazycodeboy/react-native-splash-screen)
19 |
20 | ## Features
21 |
22 | In an existing React Native project, our generator contains sub-generators that will help you with:
23 |
24 | - Setup
25 | - [Linting](generators/lint/README.md) -- `yo rn-toolbox:lint`
26 | - [Base project](generators/base/README.md) -- `yo rn-toolbox:base`
27 | - [Advanced project](generators/advanced-base/README.md) -- `yo rn-toolbox:advanced-base`
28 | - [Jest](generators/jest/README.md) -- `yo rn-toolbox:jest`
29 | - [Fastlane setup for multiple environments](generators/fastlane-setup/README.md) -- `yo rn-toolbox:fastlane-setup`
30 | - [Fastlane environment instantiation](generators/fastlane-env/README.md) -- `yo rn-toolbox:fastlane-env`
31 | - [Icons and Splashscreen generation](generators/assets/README.md) -- `yo rn-toolbox:assets [--icon | --splash] `
32 | - Environment
33 | - [Visual Studio Code](generators/vscode/README.md) -- `yo rn-toolbox:vscode`
34 | - Continuous integration / deployment
35 | - [TravisCI](generators/travisci/README.md) -- `yo rn-toolbox:travisci`
36 | - [Bitrise for continuous deployment](generators/bitrise/README.md) -- `yo rn-toolbox:bitrise`
37 | - [CircleCI for continuous deployment](generators/circleci/README.md) -- `yo rn-toolbox:circleci`
38 |
39 | ## Requirements
40 |
41 | - [ ] You need `node > 6` installed
42 | - [ ] Ruby > `2.2.3`
43 | - [ ] Bundler installed (`gem install bundler`)
44 | - [ ] Yeoman installed (`npm i -g yo`)
45 | - [ ] Yarn installed (`brew install yarn`)
46 |
47 | ## Usage
48 |
49 | Install the main `yeoman` generator:
50 |
51 | ```
52 | npm install -g yo generator-rn-toolbox
53 | ```
54 |
55 | Then follow the docs for any sub-generator listed above in the [features](https://github.com/bamlab/generator-rn-toolbox#features).
56 |
57 | If starting from scratch, use the `react-native init && cd ` command to instantiate your React Native Project (for more [go see the official React Native getting started](https://facebook.github.io/react-native/docs/getting-started.html)).
58 |
59 | It is recommended to initiate the git repository right after instantiating the app and to do you first commit.
60 |
61 | It is also recommended to do a separate commit after running each of these steps.
62 |
63 | ## Contributing
64 |
65 | See [our contributing guidelines](https://bamlab.github.io/open-source/#contributing)
66 |
67 | ### Local development
68 |
69 | To run the generator with your local version:
70 |
71 | ```shell
72 | git clone https://github.com/bamlab/generator-rn-toolbox.git
73 | cd generator-rn-toolbox
74 | npm link
75 | ```
76 |
77 | When you're done, you can run `npm unlink` to stop using your local version.
78 |
79 | ### Disclaimer
80 |
81 | _To better understand your usage of this tool, basic analytics have been enabled. It only records generators usage as anonymous page views and does not track any user information_
82 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/analytics.js:
--------------------------------------------------------------------------------
1 | const ua = require('universal-analytics');
2 |
3 | const analytics = ua('UA-145385834-1');
4 |
5 | module.exports = analytics;
6 |
--------------------------------------------------------------------------------
/assets/logos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bamlab/generator-rn-toolbox/1e5604124c0808e9ce448865c2f90d4847f1e4d3/assets/logos.png
--------------------------------------------------------------------------------
/generators/advanced-base/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Base Project setup
2 |
3 | *Tired of the same empty initial app? Start with a real React Native Project*
4 |
5 | Use `yo rn-toolbox:advanced-base` to replace the base project
6 |
7 | This generator is an alternative to `yo rn-toolbox:base`. It add a basic navigation and some common component to start faster.
8 |
9 | **Features**
10 | - Centralized App.js
11 | - [react-navigation](https://github.com/react-community/react-navigation) 5 pages setup
12 | - Custom header
13 | - Custom button
14 | - Centralized app style in a theme
15 | - Page container
16 | - 2 dashbord page with tab navigation
17 | - A signup and a Login page with some input fields
18 | - A platform specific design
19 | - i18n support
20 |
--------------------------------------------------------------------------------
/generators/advanced-base/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const analytics = require('../../analytics');
3 |
4 | class BaseGenerator extends Base {
5 | initializing() {
6 | analytics.pageview('/advanced-base').send();
7 | this.composeWith(require.resolve('../checkversion'));
8 | }
9 |
10 | prompting() {
11 | const config = this.fs.readJSON(this.destinationPath('package.json'));
12 | return this.prompt([
13 | {
14 | type: 'input',
15 | name: 'appName',
16 | message: 'Your react native app directory name',
17 | default: config.name,
18 | },
19 | ]).then(answers => {
20 | this.answers = answers;
21 | });
22 | }
23 |
24 | install() {
25 | this.yarnInstall(
26 | [
27 | 'babel-preset-react-native-stage-0',
28 | 'react-navigation',
29 | 'react-native-vector-icons',
30 | 'react-native-i18n',
31 | ],
32 | { cwd: this.destinationRoot() }
33 | ).then(() => {
34 | this.spawnCommand('react-native', ['link']);
35 | });
36 | }
37 |
38 | writing() {
39 | this.fs.delete(this.destinationPath('__tests__'));
40 | this.fs.delete(this.destinationPath('App.js'));
41 | this.fs.copyTpl(
42 | this.templatePath('**/*.js'),
43 | this.destinationPath(''),
44 | this.answers
45 | );
46 | this.fs.copy(this.templatePath('**/*.json'), this.destinationPath(''));
47 | this.fs.copy(this.templatePath('**/*.png'), this.destinationPath(''));
48 | this.fs.copy(this.templatePath('**/*.jpg'), this.destinationPath(''));
49 | this.fs.copyTpl(
50 | this.templatePath('babelrc'),
51 | this.destinationPath('.babelrc')
52 | );
53 | }
54 |
55 | end() {
56 | this.config.set('advanced-base', true);
57 | this.config.save();
58 | }
59 | }
60 |
61 | module.exports = BaseGenerator;
62 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native", "react-native-stage-0/decorator-support"]
3 | }
4 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/index.js:
--------------------------------------------------------------------------------
1 | import { AppRegistry } from 'react-native';
2 | import App from '<%= appName %>/src/App';
3 |
4 | AppRegistry.registerComponent('<%= appName %>', () => App);
5 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Scenes from './Scenes';
3 |
4 | const App = () => ;
5 |
6 | export default App;
7 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/Scenes.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React from 'react';
3 | import {
4 | createStackNavigator,
5 | createTabNavigator,
6 | HeaderBackButton,
7 | } from 'react-navigation';
8 |
9 | import * as Pages from './pages';
10 | import theme from 'theme';
11 | import I18n from 'lib/i18n';
12 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
13 |
14 | const TabIcon = props => ;
15 |
16 | const LandingStack = createStackNavigator(
17 | {
18 | landing: {
19 | screen: Pages.Landing,
20 | },
21 | login: {
22 | screen: Pages.Login,
23 | },
24 | },
25 | {
26 | initialRouteName: 'landing',
27 | mode: 'modal',
28 | headerMode: 'none',
29 | navigationOptions: {
30 | header: null,
31 | },
32 | }
33 | );
34 |
35 | const SignUpStack = createStackNavigator(
36 | {
37 | signup: {
38 | screen: Pages.Signup,
39 | navigationOptions: props => ({
40 | title: I18n.t('signup.title'),
41 | headerLeft: (
42 | {
44 | props.navigation.goBack(null);
45 | }}
46 | tintColor={theme.colors.overPrimary}
47 | />
48 | ),
49 | }),
50 | },
51 | },
52 | {
53 | initialRouteName: 'signup',
54 | navigationOptions: {
55 | headerTintColor: theme.colors.overPrimary,
56 | headerStyle: {
57 | backgroundColor: theme.colors.primary,
58 | },
59 | },
60 | }
61 | );
62 |
63 | const DashboardTab = createTabNavigator(
64 | {
65 | home: {
66 | screen: Pages.Home,
67 | navigationOptions: {
68 | title: I18n.t('home.title'),
69 | tabBarIcon: props => ,
70 | },
71 | },
72 | account: {
73 | screen: Pages.Account,
74 | navigationOptions: {
75 | title: I18n.t('account.title'),
76 | tabBarIcon: props => ,
77 | },
78 | },
79 | },
80 | {
81 | initialRouteName: 'home',
82 | animationEnabled: true,
83 | backBehavior: 'initialRoute',
84 | tabBarOptions: {
85 | activeTintColor: theme.colors.primary,
86 | pressColor: theme.colors.grayLight,
87 | inactiveTintColor: theme.colors.gray,
88 | indicatorStyle: {
89 | backgroundColor: theme.colors.primary,
90 | height: 3,
91 | },
92 | style: {
93 | backgroundColor: 'white',
94 | },
95 | },
96 | }
97 | );
98 |
99 | const DashboardStack = createStackNavigator(
100 | {
101 | dashboardTabs: {
102 | screen: DashboardTab,
103 | navigationOptions: ({ navigationOptions }) => ({
104 | ...navigationOptions,
105 | headerStyle: {
106 | ...navigationOptions.headerStyle,
107 | elevation: 0,
108 | },
109 | }),
110 | },
111 | },
112 | {
113 | initialRouteName: 'dashboardTabs',
114 | navigationOptions: {
115 | headerTintColor: theme.colors.overPrimary,
116 | headerStyle: {
117 | backgroundColor: theme.colors.primary,
118 | },
119 | },
120 | }
121 | );
122 |
123 | export const RootNavigator = createStackNavigator(
124 | {
125 | landing: {
126 | screen: LandingStack,
127 | },
128 | signup: {
129 | screen: SignUpStack,
130 | },
131 | dashboard: {
132 | screen: DashboardStack,
133 | },
134 | },
135 | {
136 | initialRouteName: 'landing',
137 | headerMode: 'none',
138 | }
139 | );
140 |
141 | export default RootNavigator;
142 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/Button.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 | import { Text, StyleSheet } from 'react-native';
4 |
5 | import Touchable from './Touchable';
6 |
7 | import theme from 'theme';
8 |
9 | export type Props = {
10 | text?: string,
11 | onPress: () => void,
12 | children?: React.Element<*>,
13 | style?: any,
14 | textStyle?: any,
15 | };
16 |
17 | type DefaultProps = {
18 | onPress: () => void,
19 | };
20 |
21 | class Button extends Component {
22 | static defaultProps: DefaultProps = {
23 | onPress: () => {},
24 | };
25 |
26 | render() {
27 | const { style, text, textStyle, children, ...rest } = this.props;
28 | const content = text
29 | ?
30 | {text.toUpperCase()}
31 |
32 | : children;
33 |
34 | return (
35 |
36 | {content}
37 |
38 | );
39 | }
40 | }
41 |
42 | const styles = StyleSheet.create({
43 | button: {
44 | alignSelf: 'stretch',
45 | justifyContent: 'center',
46 | height: 40,
47 | backgroundColor: theme.colors.primary,
48 | paddingHorizontal: 8,
49 | borderRadius: 5,
50 | marginVertical: 8,
51 | },
52 | text: {
53 | ...theme.fonts.button,
54 | color: theme.colors.overPrimary,
55 | textAlign: 'center',
56 | },
57 | });
58 |
59 | export default Button;
60 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/ButtonCard.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { PureComponent } from 'react';
3 | import { StyleSheet, View, Text } from 'react-native';
4 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
5 |
6 | import { Touchable } from 'components';
7 | import theme from 'theme';
8 |
9 | type Props = {
10 | text: string,
11 | onPress: ?Function,
12 | };
13 |
14 | class ButtonCard extends PureComponent {
15 | render() {
16 | return (
17 |
18 |
19 |
20 | {this.props.text}
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | const styles = StyleSheet.create({
30 | card: {
31 | backgroundColor: 'white',
32 | alignSelf: 'stretch',
33 | paddingHorizontal: theme.defaultPadding / 2,
34 | },
35 | contentContainer: {
36 | borderBottomWidth: 1,
37 | borderColor: theme.colors.grayLighter,
38 |
39 | flexDirection: 'row',
40 | justifyContent: 'space-between',
41 | alignItems: 'center',
42 | minHeight: 55,
43 | },
44 | text: {
45 | ...theme.fonts.button,
46 | },
47 | });
48 |
49 | export default ButtonCard;
50 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/Page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { View, StyleSheet, Image, Dimensions } from 'react-native';
3 | import theme from 'theme';
4 |
5 | type Props = {
6 | children: React.Element<*>,
7 | noPadding: boolean,
8 | noNavBar: boolean,
9 | backgroundColor: string,
10 | style?: any,
11 | backgroundImage?: any,
12 | };
13 |
14 | type DefaultProps = {
15 | children: React.Element<*>,
16 | noPadding: boolean,
17 | noNavBar: boolean,
18 | backgroundColor: string,
19 | };
20 |
21 | class Page extends Component {
22 | static defaultProps: DefaultProps = {
23 | children: null,
24 | noPadding: false,
25 | noNavBar: false,
26 | backgroundColor: theme.colors.background,
27 | };
28 |
29 | render() {
30 | const containerStyle = StyleSheet.flatten([
31 | styles.page,
32 | {
33 | paddingTop: this.props.noNavBar ? 0 : 16,
34 | paddingHorizontal: this.props.noPadding ? 0 : 32,
35 | backgroundColor: this.props.backgroundColor,
36 | },
37 | this.props.style,
38 | ]);
39 |
40 | return (
41 |
42 | {this.props.backgroundImage &&
43 | }
44 | {this.props.children}
45 |
46 | );
47 | }
48 | }
49 |
50 | const styles = StyleSheet.create({
51 | page: {
52 | flex: 1,
53 | flexDirection: 'column',
54 | justifyContent: 'flex-start',
55 | },
56 | image: {
57 | position: 'absolute',
58 | top: 0,
59 | left: 0,
60 | width: Dimensions.get('window').width,
61 | height: Dimensions.get('window').height,
62 | },
63 | });
64 |
65 | export default Page;
66 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/ProfileHeader.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 | import { StyleSheet, View, Image, Text, ActivityIndicator } from 'react-native';
4 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
5 |
6 | import { Page, Touchable } from 'components';
7 | import theme from 'theme';
8 |
9 | type Props = {
10 | user: ?Object,
11 | onPress: ?Function,
12 | };
13 |
14 | class ProfileHeader extends Component {
15 | render() {
16 | const { user } = this.props;
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | {user
25 | ?
26 | : }
27 |
28 |
29 |
30 | {user &&
31 |
32 |
33 | {user.firstName} {user.lastName}
34 |
35 | }
36 |
37 | );
38 | }
39 | }
40 |
41 | const styles = StyleSheet.create({
42 | container: {
43 | backgroundColor: theme.colors.primary,
44 | alignSelf: 'stretch',
45 | alignItems: 'center',
46 | paddingVertical: 16,
47 | },
48 | imageContainer: {
49 | marginHorizontal: Page.DEFAULT_PADDING,
50 | },
51 | userImage: {
52 | height: 70,
53 | width: 70,
54 | borderRadius: 35,
55 | borderWidth: 2,
56 | borderColor: 'white',
57 | },
58 | name: {
59 | color: 'white',
60 | marginTop: 10,
61 | fontSize: 18,
62 | textAlign: 'center',
63 | },
64 | imageOverlay: {
65 | position: 'absolute',
66 | top: 0,
67 | height: 70,
68 | width: 70,
69 | borderRadius: 35,
70 |
71 | backgroundColor: '#00000055',
72 | justifyContent: 'center',
73 | alignItems: 'center',
74 | },
75 | });
76 |
77 | export default ProfileHeader;
78 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/SecondaryFlatButton.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 | import { StyleSheet } from 'react-native';
4 |
5 | import theme from 'theme';
6 |
7 | import Button from './Button';
8 | import type { Props } from './Button';
9 |
10 | class SecondaryFlatButton extends Component {
11 | render() {
12 | return (
13 |
20 | );
21 | }
22 | }
23 |
24 | const styles = StyleSheet.create({
25 | button: {
26 | backgroundColor: 'transparent',
27 | },
28 | textStyle: {
29 | ...theme.fonts.secondaryFlatButton,
30 | },
31 | });
32 |
33 | export default SecondaryFlatButton;
34 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/TextInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import { Text, TextInput as RNTextInput, View, StyleSheet } from 'react-native';
4 | import theme from 'theme';
5 |
6 | type Props = {
7 | errorMessage?: string,
8 | type: 'name' | 'text' | 'email' | 'password' | 'digits' | 'numeric',
9 | containerStyle?: PropTypes.number,
10 | style?: StyleSheet.Styles | Array,
11 | };
12 |
13 | class TextInput extends Component {
14 | getTypeProps() {
15 | switch (this.props.type) {
16 | case 'name':
17 | return {
18 | autoCorrect: false,
19 | };
20 | case 'email':
21 | return {
22 | autoCorrect: false,
23 | keyboardType: 'email-address',
24 | autoCapitalize: 'none',
25 | };
26 | case 'password':
27 | return {
28 | autoCorrect: false,
29 | secureTextEntry: true,
30 | };
31 | case 'digits': {
32 | return {
33 | keyboardType: 'phone-pad',
34 | };
35 | }
36 | case 'numeric': {
37 | return {
38 | keyboardType: 'numeric',
39 | };
40 | }
41 | default:
42 | return {};
43 | }
44 | }
45 |
46 | clear() {
47 | this.refs.textinputRef.clear();
48 | }
49 |
50 | focus() {
51 | this.refs.textinputRef.focus();
52 | }
53 |
54 | render() {
55 | const { errorMessage, containerStyle, style, type, ...inputProps } = this.props;
56 | return (
57 |
58 |
65 |
73 |
74 | {errorMessage &&
75 |
76 | {errorMessage}
77 | }
78 |
79 | );
80 | }
81 | }
82 |
83 | const styles = StyleSheet.create({
84 | container: {
85 | marginVertical: 5,
86 | },
87 | fieldContainer: {
88 | alignSelf: 'stretch',
89 | height: 48,
90 | backgroundColor: theme.colors.inputBackground,
91 | borderWidth: 1,
92 | borderColor: theme.colors.grayLighter,
93 | borderRadius: 4,
94 | paddingHorizontal: 8,
95 | },
96 | fieldContainerError: {
97 | borderColor: theme.colors.error,
98 | },
99 | fieldText: {
100 | backgroundColor: 'transparent',
101 | flex: 1,
102 | ...theme.fonts.input,
103 | },
104 | error: {
105 | textAlign: 'right',
106 | color: theme.colors.error,
107 | backgroundColor: 'transparent',
108 | },
109 | });
110 |
111 | export default TextInput;
112 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/Touchable.js:
--------------------------------------------------------------------------------
1 | /* @flow */
2 |
3 | /**
4 | * TouchableItem renders a touchable that looks native on both iOS and Android.
5 | *
6 | * It provides an abstraction on top of TouchableNativeFeedback and
7 | * TouchableOpacity.
8 | *
9 | * On iOS you can pass the props of TouchableOpacity, on Android pass the props
10 | * of TouchableNativeFeedback.
11 | */
12 | import React, { Component, Children } from 'react';
13 | import { Platform, TouchableNativeFeedback, TouchableOpacity, View } from 'react-native';
14 |
15 | const ANDROID_VERSION_LOLLIPOP = 21;
16 |
17 | type Props = {
18 | onPress?: Function,
19 | delayPressIn?: number,
20 | borderless?: boolean,
21 | pressColor?: ?string,
22 | activeOpacity?: number,
23 | children?: React.Element<*>,
24 | style?: any,
25 | disabled?: ?boolean,
26 | disabledOpacity?: number,
27 | useOpacity?: boolean,
28 | };
29 |
30 | type DefaultProps = {
31 | pressColor: ?string,
32 | activeOpacity: number,
33 | disabled: ?boolean,
34 | disabledOpacity: number,
35 | };
36 |
37 | export default class Touchable extends Component {
38 | static defaultProps: DefaultProps = {
39 | pressColor: 'rgba(0, 0, 0, .32)',
40 | activeOpacity: 0.7,
41 | disabled: false,
42 | disabledOpacity: 0.4,
43 | useOpacity: false,
44 | };
45 |
46 | render() {
47 | const { style, onPress, disabled, disabledOpacity, useOpacity, ...rest } = this.props;
48 | const disabableOnPress = disabled ? undefined : onPress;
49 | const finalStyle = [{ opacity: disabled ? disabledOpacity : 1 }, style];
50 |
51 | if (Platform.OS === 'android' && Platform.Version >= ANDROID_VERSION_LOLLIPOP && !useOpacity) {
52 | return (
53 |
60 |
61 | {Children.only(this.props.children)}
62 |
63 |
64 | );
65 | }
66 |
67 | return (
68 |
69 |
70 | {this.props.children}
71 |
72 |
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/__tests__/Button.test.js:
--------------------------------------------------------------------------------
1 | import 'react-native';
2 | import React from 'react';
3 | import renderer from 'react-test-renderer';
4 |
5 | import Button from '../Button';
6 |
7 | describe('Button', () => {
8 | it('should render the button', () => {
9 | const tree = renderer.create().toJSON();
10 | expect(tree).toMatchSnapshot();
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/__tests__/__snapshots__/Button.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Button should render the button 1`] = `
4 |
28 |
49 |
66 | HELLO
67 |
68 |
69 |
70 | `;
71 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/components/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @providesModule components
3 | */
4 |
5 | /**
6 | * This syntax prevent circular import
7 | * prevent some break in hot reaload
8 | * and optimize the number of file imported
9 | */
10 | module.exports = {
11 | get Page() {
12 | return require('./Page').default;
13 | },
14 | get Button() {
15 | return require('./Button').default;
16 | },
17 | get TextInput() {
18 | return require('./TextInput').default;
19 | },
20 | get SecondaryFlatButton() {
21 | return require('./SecondaryFlatButton').default;
22 | },
23 | get ProfileHeader() {
24 | return require('./ProfileHeader').default;
25 | },
26 | get ButtonCard() {
27 | return require('./ButtonCard').default;
28 | },
29 | get Touchable() {
30 | return require('./Touchable').default;
31 | },
32 | };
33 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/lib/i18n.js:
--------------------------------------------------------------------------------
1 | // @flow weak
2 | import I18n from 'react-native-i18n';
3 | import translations from '<%= appName %>/translations';
4 |
5 | I18n.fallbacks = true;
6 | I18n.translations = translations;
7 | I18n.defaultLocale = 'en';
8 |
9 | I18n.has = key => I18n.locale in translations && key in translations[I18n.locale];
10 |
11 | export default I18n;
12 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/lib/package.json:
--------------------------------------------------------------------------------
1 | { "name": "lib" }
2 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/pages/Account.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 |
4 | import { NavigationActions, StackActions } from 'react-navigation';
5 | import type { NavigationTabScreenOptions } from 'react-navigation';
6 |
7 | import { Page, ProfileHeader, ButtonCard } from 'components';
8 |
9 | import I18n from 'lib/i18n';
10 |
11 | type Props = {
12 | navigation: any,
13 | };
14 |
15 | class Account extends Component {
16 | _logout() {
17 | // @see https://github.com/react-community/react-navigation/issues/1127
18 | const resetAction = StackActions.reset({
19 | index: 0,
20 | key: null,
21 | actions: [NavigationActions.navigate({ routeName: 'landing' })],
22 | });
23 | this.props.navigation.dispatch(resetAction);
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 | this._logout()}
32 | text={I18n.t('account.logout')}
33 | />
34 |
35 | );
36 | }
37 | }
38 | export default Account;
39 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 | import { StyleSheet, Text, View } from 'react-native';
4 |
5 | import type { NavigationTabScreenOptions } from 'react-navigation';
6 | import { Page } from 'components';
7 | import theme from 'theme';
8 | import I18n from 'lib/i18n';
9 |
10 | type Props = {
11 | navigation: any,
12 | };
13 |
14 | class Home extends Component {
15 | render() {
16 | return (
17 |
18 |
19 |
20 | {I18n.t('home.text')}
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | flex: 1,
31 | justifyContent: 'center',
32 | alignItems: 'center',
33 | },
34 | title: {
35 | ...theme.fonts.pageTitle,
36 | },
37 | });
38 |
39 | export default Home;
40 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/pages/Landing.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 | import { StyleSheet, View, Image } from 'react-native';
4 |
5 | import { Page, Button, SecondaryFlatButton } from 'components';
6 | import theme from 'theme';
7 | import I18n from 'lib/i18n';
8 |
9 | type Props = {
10 | navigation: any,
11 | };
12 |
13 | class Landing extends Component {
14 | render() {
15 | return (
16 |
17 |
18 |
19 |
20 |
21 |
30 |
31 | );
32 | }
33 | }
34 |
35 | const styles = StyleSheet.create({
36 | content: {
37 | justifyContent: 'space-around',
38 | },
39 | imageContainer: {
40 | alignItems: 'center',
41 | },
42 | });
43 |
44 | export default Landing;
45 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/pages/Login.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | import React, { Component } from 'react';
3 | import { View, StyleSheet } from 'react-native';
4 | import { NavigationActions, StackActions } from 'react-navigation';
5 |
6 | import { Page, Button, TextInput, SecondaryFlatButton } from 'components';
7 | import theme from 'theme';
8 | import I18n from 'lib/i18n';
9 |
10 | type Props = {
11 | navigation: any,
12 | };
13 |
14 | class Login extends Component {
15 | _goToHomePage() {
16 | // @see https://github.com/react-community/react-navigation/issues/1127
17 | const resetAction = StackActions.reset({
18 | index: 0,
19 | key: null,
20 | actions: [NavigationActions.navigate({ routeName: 'dashboard' })],
21 | });
22 | this.props.navigation.dispatch(resetAction);
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |
29 |
30 |
34 |
35 |
44 |
45 | );
46 | }
47 | }
48 |
49 | const styles = StyleSheet.create({
50 | page: {
51 | paddingTop: 50,
52 | },
53 | });
54 |
55 | export default Login;
56 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/pages/Signup.js:
--------------------------------------------------------------------------------
1 | // @flo
2 | import React, { Component } from 'react';
3 | import { StyleSheet, View } from 'react-native';
4 | import { NavigationActions, StackActions } from 'react-navigation';
5 |
6 | import type { NavigationTabScreenOptions } from 'react-navigation';
7 | import { Page, Button, TextInput } from 'components';
8 | import theme from 'theme';
9 | import I18n from 'lib/i18n';
10 |
11 | type Props = {
12 | navigator: any,
13 | };
14 |
15 | class Signup extends Component {
16 | _goToHomePage() {
17 | // @see https://github.com/react-community/react-navigation/issues/1127
18 | const resetAction = StackActions.reset({
19 | index: 0,
20 | key: null,
21 | actions: [NavigationActions.navigate({ routeName: 'dashboard' })],
22 | });
23 | this.props.navigation.dispatch(resetAction);
24 | }
25 |
26 | render() {
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
43 |
44 | );
45 | }
46 | }
47 |
48 | const styles = StyleSheet.create({
49 | container: {
50 | flex: 1,
51 | },
52 | });
53 |
54 | export default Signup;
55 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/pages/index.js:
--------------------------------------------------------------------------------
1 | export { default as Home } from './Home';
2 | export { default as Account } from './Account';
3 | export { default as Landing } from './Landing';
4 | export { default as Login } from './Login';
5 | export { default as Signup } from './Signup';
6 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/theme/images/default-user-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bamlab/generator-rn-toolbox/1e5604124c0808e9ce448865c2f90d4847f1e4d3/generators/advanced-base/templates/src/theme/images/default-user-image.png
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/theme/images/landing.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bamlab/generator-rn-toolbox/1e5604124c0808e9ce448865c2f90d4847f1e4d3/generators/advanced-base/templates/src/theme/images/landing.jpg
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/theme/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bamlab/generator-rn-toolbox/1e5604124c0808e9ce448865c2f90d4847f1e4d3/generators/advanced-base/templates/src/theme/images/logo.png
--------------------------------------------------------------------------------
/generators/advanced-base/templates/src/theme/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @providesModule theme
3 | */
4 | const colors = {
5 | primary: '#3f51b5',
6 | secondary: '#e64a19',
7 | lightText: '#FAFAFA',
8 | background: '#eeeeee',
9 |
10 | darkGray: '#333333',
11 | gray: '#777777',
12 | grayLight: '#999999',
13 | grayLighter: '#AAAAAA',
14 | extraLightGray: '#DDDDDD',
15 |
16 | inputBackground: 'white',
17 |
18 | overPrimary: 'white',
19 | error: '#FF4444',
20 | };
21 |
22 | export default {
23 | colors,
24 | images: {
25 | landing: require('./images/landing.jpg'),
26 | logo: require('./images/logo.png'),
27 | defaultUserImage: require('./images/default-user-image.png'),
28 | },
29 | fonts: {
30 | pageTitle: {
31 | fontSize: 20,
32 | color: colors.darkGray,
33 | textAlign: 'center',
34 | backgroundColor: 'transparent',
35 | },
36 | button: {
37 | fontSize: 14,
38 | fontWeight: '700',
39 | color: colors.darkGray,
40 | },
41 | placeholder: {
42 | color: colors.gray,
43 | },
44 | secondaryFlatButton: {
45 | color: colors.extraLightGray,
46 | fontWeight: '400',
47 | },
48 | input: {
49 | color: colors.darkGrey,
50 | },
51 | },
52 | defaultPadding: 32,
53 | };
54 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/translations/en/translations.json:
--------------------------------------------------------------------------------
1 | {
2 | "account": {
3 | "logout": "Logout",
4 | "title": "Account"
5 | },
6 | "home": {
7 | "text": "This is the home page !",
8 | "title": "Home"
9 | },
10 | "landing": {
11 | "login": "Already have an account?",
12 | "register": "Register"
13 | },
14 | "login": {
15 | "login": "Login",
16 | "signup": "I don't have an account"
17 | },
18 | "signup": {
19 | "signup": "Signup",
20 | "title": "Signup"
21 | },
22 | "user": {
23 | "form": {
24 | "email": "Email",
25 | "firstname": "First name",
26 | "lastname": "Last name",
27 | "password": "Password"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/translations/fr/translations.json:
--------------------------------------------------------------------------------
1 | {
2 | "account": {
3 | "logout": "Déconnexion",
4 | "title": "Mon compte"
5 | },
6 | "home": {
7 | "text": "Ceci est la page d'accueil",
8 | "title": "Accueil"
9 | },
10 | "landing": {
11 | "login": "Déjà un compte ?",
12 | "register": "S'inscrire"
13 | },
14 | "login": {
15 | "login": "Connexion",
16 | "signup": "Pas encore de compte ?"
17 | },
18 | "signup": {
19 | "signup": "S'inscrire",
20 | "title": "Inscription"
21 | },
22 | "user": {
23 | "form": {
24 | "email": "Email",
25 | "firstname": "Prénom",
26 | "lastname": "Nom",
27 | "password": "Mot de passe"
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/generators/advanced-base/templates/translations/index.js:
--------------------------------------------------------------------------------
1 | // @flow weak
2 | import en from './en/translations.json';
3 | import fr from './fr/translations.json';
4 |
5 | const translations = {
6 | en,
7 | fr,
8 | };
9 |
10 | module.exports = translations;
11 |
--------------------------------------------------------------------------------
/generators/app/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const path = require('path');
3 | const fs = require('fs');
4 |
5 | const generatorList = fs
6 | .readdirSync(path.join(__dirname, '..'))
7 | .filter(generatorName => generatorName !== 'app');
8 |
9 | class BaseGenerator extends Base {
10 | prompting() {
11 | return this.prompt([
12 | {
13 | type: 'list',
14 | name: 'generator',
15 | message: 'which generator do you want to run',
16 | choices: generatorList,
17 | },
18 | ]).then(answers => {
19 | this.generator = answers.generator;
20 | });
21 | }
22 |
23 | main() {
24 | this.composeWith(
25 | `rn-toolbox:${this.generator}`,
26 | {},
27 | {
28 | local: require.resolve(path.join('..', this.generator)),
29 | }
30 | );
31 | }
32 | }
33 |
34 | module.exports = BaseGenerator;
35 |
--------------------------------------------------------------------------------
/generators/assets/README.md:
--------------------------------------------------------------------------------
1 | # Assets setup
2 |
3 | ## Requirements
4 |
5 | - You need node >= 6 installed (using [NVM](https://github.com/creationix/nvm) is recommended)
6 | - Install the generator `npm install -g yo generator-rn-toolbox`
7 | - You need `imagemagick` installed to use this generator.
8 |
9 | On a mac, you can install it with `brew`:
10 | ```
11 | brew install imagemagick
12 | ```
13 |
14 | ## Generate icons
15 | You'll need a **square** image for your icon with a size of more than **192x192 px** (psd is supported).
16 |
17 | ### iOS
18 | Starting with XCode 9, your Apple App Store icon is bundled with the app, so provide an icon with a size of more than **1024x1024 px** (psd is supported).
19 |
20 | Then run:
21 | ```
22 | yo rn-toolbox:assets --icon icon.png
23 | ```
24 | Answer yes when asked about overwriting.
25 |
26 | That's it! :balloon:
27 | Icons have been generated in different sizes and integrated in your project.
28 |
29 |
30 | ### Android
31 | - Open your project in Android Studio
32 | - Right click on the `app` folder, then click on `New` and then click on `Image Asset`. If you don't see this menu, be sure that you got the latest gradle version (should work with gradle >=3.4.2)
33 | - In the `Icon Type` field select `Launcher Icons (Adaptative and Legacy)`
34 | - In the `path`field, select your image file
35 | - Once done, click on the `Next`button and then on the finish `button`
36 |
37 | ## Generate splashscreens
38 |
39 | You'll need a **square** image for your splash with a size of more than **2208x2208 px** (psd is supported). The image will be cropped, it should be the logo of the app inside a big square with a 30% margin.
40 | Be aware that content located on the edges of the picture might get cut (see [example with bottom content](./doc/splashscreen-with-bottom-content.png) and [example with right side content](./doc/splashscreen-with-right-side-content.png) where the logo is cut).
41 |
42 | ### iOS
43 |
44 | Run:
45 | ```
46 | yo rn-toolbox:assets --splash splash.psd --ios
47 | ```
48 |
49 | You're all set! :dancer:
50 |
51 | ***IMPORTANT:*** You will need to uninstall the app from device/emulator first before seeing the changes.
52 |
53 |
54 | If you want to adjust the margin of your image, you can run this command in your terminal :
55 | ```
56 | convert -background -gravity center -extent
57 | ```
58 | For instance :
59 | ```
60 | convert ./splashcreen.png -background 'white' -gravity center -extent 3200x3200 splash.png
61 | ```
62 | will generate a new image (splash.png) with a size of 3200x3200 and will place the old image (splashcreen.png) in the center, surrounded by white color
63 |
64 | ### Android
65 |
66 | The generator only supports a splash with a plain background color on Android.
67 |
68 | Run:
69 | ```
70 | yo rn-toolbox:assets --splash splash.psd --android
71 | ```
72 |
73 | In order to set the background color go in YOURPROJECT/android/app/src/main/res/values/colors.xml and put an hexacode instead of the `rgb(0,0,0)`.
74 |
75 | You're done! :dancer:
76 |
77 | ***IMPORTANT:*** You will need to uninstall the app from device/emulator first before seeing the changes.
78 |
79 |
80 | ## Generate Store assets
81 |
82 | You'll need to specify:
83 | - an icon with a size of more than **512x512 px** (psd is supported).
84 | - a splash screen image for the play store image with a size of more than **1024x500 px** (psd is supported).
85 |
86 | ```
87 | yo rn-toolbox:assets --icon icon.png --splash splash.psd --store
88 | ```
89 |
90 | This will create 3 files at the root of your project:
91 | - `itunes-icon.png`
92 | - `play-store-icon.png`
93 | - `play-store-image.png`
94 |
95 | ## Generate Android notification icons
96 |
97 | When setting up push notifications on Android (with [React Native Push notification](https://github.com/zo0r/react-native-push-notification) for instance), you'll need a [status bar icon](https://developer.android.com/guide/practices/ui_guidelines/icon_design_status_bar.html).
98 |
99 | You'll need an image for your icon with a size of more than **96x96 px** (psd is supported).
100 | ```
101 | yo rn-toolbox:assets --android-notification-icon icon.png
102 | ```
103 |
104 | ## Run the command only for a platform
105 | You can select the platform you want to generate assets for. For instance:
106 | ```
107 | yo rn-toolbox:assets --icon icon.png --android
108 | yo rn-toolbox:assets --splash splash.psd --ios
109 | ```
110 |
111 | ## Hide Splashscreen from JS code
112 |
113 | You can use [react-native-splash-screen](https://github.com/crazycodeboy/react-native-splash-screen) for iOS.
114 |
115 | ## Troubleshooting
116 |
117 | - Getting `Error: Command failed: Invalid Parameter - -resize` on **Windows**
118 |
119 | Reinstalling ImageMagick with the legacy tools should do the trick:
120 |
121 | 
122 |
--------------------------------------------------------------------------------
/generators/assets/doc/splashscreen-with-bottom-content.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bamlab/generator-rn-toolbox/1e5604124c0808e9ce448865c2f90d4847f1e4d3/generators/assets/doc/splashscreen-with-bottom-content.png
--------------------------------------------------------------------------------
/generators/assets/doc/splashscreen-with-right-side-content.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bamlab/generator-rn-toolbox/1e5604124c0808e9ce448865c2f90d4847f1e4d3/generators/assets/doc/splashscreen-with-right-side-content.png
--------------------------------------------------------------------------------
/generators/assets/getPixelColor.js:
--------------------------------------------------------------------------------
1 | const gm = require('gm').subClass({ imageMagick: true });
2 |
3 | module.exports = (imagePath, x, y) =>
4 | new Promise((resolve, reject) =>
5 | gm(imagePath)
6 | .crop(x, y)
7 | .identify('%[hex:s]', (error, imageMagickColor) => {
8 | if (error) return reject(error);
9 | return resolve(`#${imageMagickColor}`);
10 | })
11 | );
12 |
--------------------------------------------------------------------------------
/generators/assets/imageGenerator.js:
--------------------------------------------------------------------------------
1 | const gm = require('gm').subClass({ imageMagick: true });
2 | const fs = require('fs-extra');
3 | const path = require('path');
4 | const Promise = require('bluebird');
5 | require('colors');
6 |
7 | Promise.promisifyAll(gm.prototype);
8 |
9 | /* eslint-disable no-multi-spaces */
10 | const iosIconSizes = [
11 | { size: 20, multipliers: [1, 2, 3] },
12 | { size: 29, multipliers: [1, 2, 3] },
13 | { size: 40, multipliers: [1, 2, 3] },
14 | { size: 50, multipliers: [1, 2] },
15 | { size: 57, multipliers: [1, 2] },
16 | { size: 60, multipliers: [2, 3] },
17 | { size: 72, multipliers: [1, 2] },
18 | { size: 76, multipliers: [1, 2] },
19 | { size: 83.5, multipliers: [2] },
20 | { size: 1024, multipliers: [1] },
21 | ];
22 |
23 | const androidIconSizes = [
24 | { value: 36, density: 'ldpi' },
25 | { value: 48, density: 'mdpi' },
26 | { value: 72, density: 'hdpi' },
27 | { value: 96, density: 'xhdpi' },
28 | { value: 144, density: 'xxhdpi' },
29 | { value: 192, density: 'xxxhdpi' },
30 | ];
31 |
32 | const androidSplashSizes = [
33 | { width: 320, height: 240, density: 'land-ldpi' },
34 | { width: 480, height: 320, density: 'land-mdpi' },
35 | { width: 800, height: 480, density: 'land-hdpi' },
36 | { width: 1280, height: 720, density: 'land-xhdpi' },
37 | { width: 1600, height: 960, density: 'land-xxhdpi' },
38 | { width: 1920, height: 1280, density: 'land-xxxhdpi' },
39 | { width: 240, height: 320, density: 'port-ldpi' },
40 | { width: 320, height: 480, density: 'port-mdpi' },
41 | { width: 480, height: 800, density: 'port-hdpi' },
42 | { width: 720, height: 1280, density: 'port-xhdpi' },
43 | { width: 960, height: 1600, density: 'port-xxhdpi' },
44 | { width: 1280, height: 1920, density: 'port-xxxhdpi' },
45 | ];
46 |
47 | // See http://iconhandbook.co.uk/reference/chart/android/
48 | const androidNotificationIconSizes = [
49 | { value: 24, density: 'ldpi' },
50 | { value: 24, density: 'mdpi' },
51 | { value: 36, density: 'hdpi' },
52 | { value: 48, density: 'xhdpi' },
53 | { value: 72, density: 'xxhdpi' },
54 | { value: 96, density: 'xxxhdpi' },
55 | ];
56 |
57 | const iosSplashSizes = [
58 | { name: 'Default-Portrait-812h@3x', width: 1125, height: 2436 },
59 | { name: 'Default-Landscape-812h@3x', width: 2436, height: 1125 },
60 | { name: 'Default-568h@2x', width: 640, height: 1136 },
61 | { name: 'Default-667h@2x', width: 750, height: 1334 },
62 | { name: 'Default-Portrait-736h@3x', width: 1242, height: 2208 },
63 | { name: 'Default-Landscape-736h@3x', width: 2208, height: 1242 },
64 | { name: 'Default-Landscape@2x', width: 2048, height: 1536 },
65 | { name: 'Default-Landscape', width: 1024, height: 768 },
66 | { name: 'Default-Portrait@2x', width: 1536, height: 2048 },
67 | { name: 'Default-Portrait', width: 768, height: 1024 },
68 | { name: 'Default@2x', width: 640, height: 960 },
69 | ];
70 |
71 | const itunesIconSize = { name: 'itunes-icon.png', value: 1024 };
72 |
73 | const playStoreIconSize = { name: 'play-store-icon.png', value: 512 };
74 |
75 | const playStoreImageSize = {
76 | name: 'play-store-image.png',
77 | width: 1024,
78 | height: 500,
79 | };
80 | /* eslint-enable no-multi-spaces */
81 |
82 | const getResizedImageGraphic = (sourcePath, width, height) => {
83 | const maxSize = Math.max(width, height);
84 |
85 | return gm(path.normalize(sourcePath))
86 | .resize(maxSize, maxSize)
87 | .gravity('center')
88 | .crop(
89 | width,
90 | height,
91 | height > width ? (height - width) / 2 : 0,
92 | width > height ? (width - height) / 2 : 0
93 | );
94 | };
95 |
96 | const checkImageIsSquare = sourcePath =>
97 | gm(path.normalize(sourcePath))
98 | .sizeAsync()
99 | .then(size => {
100 | if (size.width !== size.height) {
101 | console.log('Please use a square image'.red);
102 | process.exit(1);
103 | }
104 | });
105 |
106 | const generateResizedAssets = (
107 | sourcePath,
108 | destinationPath,
109 | width,
110 | givenHeight
111 | ) => {
112 | const height = givenHeight || width;
113 |
114 | const directory = path.dirname(destinationPath);
115 | if (!fs.existsSync(directory)) {
116 | fs.mkdirpSync(directory);
117 | }
118 |
119 | const psdSafeSourcePath = `${sourcePath}${
120 | sourcePath.split('.').pop() === 'psd' ? '[0]' : ''
121 | }`;
122 |
123 | return checkImageIsSquare(psdSafeSourcePath).then(() =>
124 | getResizedImageGraphic(psdSafeSourcePath, width, height)
125 | .writeAsync(path.normalize(destinationPath))
126 | .then(() => console.log(`Wrote ${destinationPath}`))
127 | );
128 | };
129 |
130 | const generateIosIcons = (iconSource, iosIconFolder) =>
131 | Promise.all(
132 | iosIconSizes.map(size =>
133 | Promise.all(
134 | size.multipliers.map(multiplier =>
135 | generateResizedAssets(
136 | iconSource,
137 | `${iosIconFolder}/icon-${size.size}@${multiplier}x.png`,
138 | size.size * multiplier
139 | )
140 | )
141 | )
142 | )
143 | );
144 |
145 | const generateAndroidIcons = (
146 | iconSource,
147 | assetsOutputPath,
148 | androidSrcDirectory
149 | ) =>
150 | Promise.all(
151 | androidIconSizes.map(size =>
152 | generateResizedAssets(
153 | iconSource,
154 | `${assetsOutputPath}/android/app/src/${androidSrcDirectory}/res/mipmap-${
155 | size.density
156 | }/ic_launcher.png`,
157 | size.value
158 | )
159 | )
160 | );
161 |
162 | const generateIosSplashScreen = (splashSource, iosSplashFolder) =>
163 | Promise.all(
164 | iosSplashSizes.map(size =>
165 | generateResizedAssets(
166 | splashSource,
167 | `${iosSplashFolder}/${size.name}.png`,
168 | size.width,
169 | size.height
170 | )
171 | )
172 | );
173 |
174 | const generateAndroidSplashScreen = (
175 | splashSource,
176 | assetsOutputPath,
177 | androidSrcDirectory
178 | ) =>
179 | androidSplashSizes.map(size =>
180 | generateResizedAssets(
181 | splashSource,
182 | `${assetsOutputPath}/android/app/src/${androidSrcDirectory}/res/drawable-${
183 | size.density
184 | }/launch_screen.png`,
185 | size.width,
186 | size.height
187 | )
188 | );
189 |
190 | const generateAndroidNotificationIcons = (
191 | iconSource,
192 | assetsOutputPath,
193 | androidSrcDirectory
194 | ) =>
195 | androidNotificationIconSizes.map(size =>
196 | generateResizedAssets(
197 | iconSource,
198 | `${assetsOutputPath}/android/app/src/${androidSrcDirectory}/res/mipmap-${
199 | size.density
200 | }/ic_notification.png`,
201 | size.value
202 | )
203 | );
204 |
205 | const generatePlayStoreIcon = iconSource =>
206 | generateResizedAssets(
207 | iconSource,
208 | playStoreIconSize.name,
209 | playStoreIconSize.value
210 | );
211 |
212 | const generateItunesIcon = iconSource =>
213 | generateResizedAssets(iconSource, itunesIconSize.name, itunesIconSize.value);
214 |
215 | const generatePlayStoreImage = iconSource =>
216 | generateResizedAssets(
217 | iconSource,
218 | playStoreImageSize.name,
219 | playStoreImageSize.width,
220 | playStoreImageSize.height
221 | );
222 |
223 | module.exports = {
224 | generateIosSplashScreen,
225 | generateAndroidIcons,
226 | generateIosIcons,
227 | generateAndroidSplashScreen,
228 | generateAndroidNotificationIcons,
229 | generatePlayStoreIcon,
230 | generateItunesIcon,
231 | generatePlayStoreImage,
232 | };
233 |
--------------------------------------------------------------------------------
/generators/assets/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const Base = require('yeoman-generator');
3 | const imageGenerator = require('./imageGenerator');
4 | const getPixelColor = require('./getPixelColor');
5 | const analytics = require('../../analytics');
6 |
7 | require('colors');
8 |
9 | class ResourcesGenerator extends Base {
10 | constructor(...args) {
11 | super(...args);
12 |
13 | this.option('icon', {
14 | type: asset => asset,
15 | desc: 'Icon source',
16 | });
17 | this.option('splash', {
18 | type: asset => asset,
19 | desc: 'Splashscreen source',
20 | });
21 | this.option('android', {
22 | desc: 'Build for Android',
23 | });
24 | this.option('iOS', {
25 | desc: 'Build for iOS',
26 | });
27 | this.option('android-notification-icon', {
28 | type: asset => asset,
29 | desc: 'Notification icon source',
30 | });
31 | this.option('store', {
32 | desc: 'Generate Stores assets',
33 | });
34 | this.option('projectName', {
35 | type: asset => asset,
36 | desc: 'Name of your react-native project',
37 | });
38 | this.option('assetsOutputPath', {
39 | type: asset => asset,
40 | desc: 'Name of your react-native project',
41 | default: '.',
42 | });
43 | this.option('androidSrcDirectory', {
44 | type: asset => asset,
45 | desc: 'The directory under `src` to save the assets',
46 | default: 'main',
47 | });
48 | this.option('iosAssetName', {
49 | type: asset => asset,
50 | desc: 'The name of the asset',
51 | default: 'AppIcon',
52 | });
53 | }
54 |
55 | initializing() {
56 | this.composeWith('rn-toolbox:checkversion');
57 | analytics.pageview('/assets').send();
58 | }
59 |
60 | prompting() {
61 | if (this.options.projectName) {
62 | this.projectName = this.options.projectName;
63 | return Promise.resolve();
64 | }
65 |
66 | const config = this.fs.readJSON(this.destinationPath('package.json'));
67 |
68 | if (!config) {
69 | this.log.error(
70 | "Could not read 'package.json' from current directory. Are you inside a React Native project?"
71 | .red
72 | );
73 | process.exit(1);
74 | }
75 |
76 | return this.prompt([
77 | {
78 | type: 'input',
79 | name: 'projectName',
80 | message: 'Name of your react-native project',
81 | required: true,
82 | default: config.name,
83 | },
84 | ]).then(answers => {
85 | this.projectName = answers.projectName;
86 | });
87 | }
88 |
89 | writing() {
90 | this._checkOSToBuildFor();
91 | this._checkAssets();
92 |
93 | return Promise.all([
94 | this._setupIosIcons(),
95 | this._setupAndroidIcons(),
96 | this._setupAndroidNotificationIcons(),
97 | this._setupIosSplashScreen(),
98 | this._setupAndroidSplashScreen(),
99 | this._setupStoresAssets(),
100 | ]);
101 | }
102 |
103 | _checkOSToBuildFor() {
104 | this.android = this.options.android || !this.options.ios;
105 | this.ios = this.options.ios || !this.options.android;
106 | }
107 |
108 | _checkAssets() {
109 | this._checkAsset('icon');
110 | this._checkAsset('splash');
111 | this._checkAsset('android-notification-icon');
112 | }
113 |
114 | _checkAsset(optionName) {
115 | const assetPath = this.options[optionName];
116 |
117 | if (assetPath && !fs.existsSync(assetPath)) {
118 | this.log.error(`${optionName} could not be found`);
119 | this.options[optionName] = null;
120 | }
121 | }
122 |
123 | _setupIosIcons() {
124 | if (!this.ios || !this.options.icon) return null;
125 |
126 | const iosIconFolder = `${this.options.assetsOutputPath}/ios/${
127 | this.projectName
128 | }/Images.xcassets/${this.options.iosAssetName}.appiconset`;
129 |
130 | this.fs.copyTpl(
131 | this.templatePath('ios/AppIconsetContents.json'),
132 | this.destinationPath(`${iosIconFolder}/Contents.json`)
133 | );
134 |
135 | return imageGenerator.generateIosIcons(this.options.icon, iosIconFolder);
136 | }
137 |
138 | _setupAndroidIcons() {
139 | if (!this.android || !this.options.icon) return null;
140 | return imageGenerator.generateAndroidIcons(
141 | this.options.icon,
142 | this.options.assetsOutputPath,
143 | this.options.androidSrcDirectory
144 | );
145 | }
146 |
147 | _setupAndroidNotificationIcons() {
148 | if (!this.options['android-notification-icon']) return null;
149 | return imageGenerator.generateAndroidNotificationIcons(
150 | this.options['android-notification-icon'],
151 | this.options.assetsOutputPath,
152 | this.options.androidSrcDirectory
153 | );
154 | }
155 |
156 | _setupIosSplashScreen() {
157 | if (!this.ios || !this.options.splash) return null;
158 |
159 | const iosSplashFolder = `${this.options.assetsOutputPath}/ios/${
160 | this.projectName
161 | }/Images.xcassets/LaunchImage.launchimage`;
162 |
163 | this.fs.copyTpl(
164 | this.templatePath('ios/LaunchImageLaunchimageContents.json'),
165 | `${iosSplashFolder}/Contents.json`
166 | );
167 |
168 | const pbxprojPath = this.destinationPath(
169 | `ios/${this.projectName}.xcodeproj/project.pbxproj`
170 | );
171 | this.fs.write(
172 | pbxprojPath,
173 | this.fs
174 | .read(pbxprojPath)
175 | .replace(
176 | /(\s*)?ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;(?:(\s*)(ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;)?)*/g,
177 | `$1ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;$1ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage;$1`
178 | )
179 | );
180 |
181 | const plistPath = this.destinationPath(
182 | `ios/${this.projectName}/Info.plist`
183 | );
184 | this.fs.write(
185 | plistPath,
186 | this.fs
187 | .read(plistPath)
188 | .replace('UILaunchStoryboardName', '')
189 | .replace('LaunchScreen', '')
190 | );
191 |
192 | return imageGenerator.generateIosSplashScreen(
193 | this.options.splash,
194 | iosSplashFolder
195 | );
196 | }
197 |
198 | _setupAndroidSplashScreen() {
199 | if (!this.android || !this.options.splash) return null;
200 |
201 | const getTopLeftPixelColor = getPixelColor(this.options.splash, 1, 1);
202 |
203 | return getTopLeftPixelColor.then(splashBackgroundColor => {
204 | this.fs.copyTpl(
205 | this.templatePath('android/colors.xml'),
206 | `${this.options.assetsOutputPath}/android/app/src/${
207 | this.options.androidSrcDirectory
208 | }/res/values/colors.xml`,
209 | { splashBackgroundColor }
210 | );
211 | this.fs.copyTpl(
212 | this.templatePath('android/launch_screen_bitmap.xml'),
213 | `${this.options.assetsOutputPath}/android/app/src/${
214 | this.options.androidSrcDirectory
215 | }/res/drawable/launch_screen_bitmap.xml`
216 | );
217 |
218 | this.fs.copyTpl(
219 | this.templatePath('android/styles.xml'),
220 | `${this.options.assetsOutputPath}/android/app/src/${
221 | this.options.androidSrcDirectory
222 | }/res/values/styles.xml`
223 | );
224 |
225 | return imageGenerator.generateAndroidSplashScreen(
226 | this.options.splash,
227 | this.options.assetsOutputPath,
228 | this.options.androidSrcDirectory
229 | );
230 | });
231 | }
232 |
233 | _setupStoresAssets() {
234 | if (!this.options.store) return null;
235 |
236 | const resizePromises = [];
237 |
238 | if (this.android && this.options.icon) {
239 | resizePromises.push(
240 | imageGenerator.generatePlayStoreIcon(this.options.icon)
241 | );
242 | }
243 | if (this.ios && this.options.icon) {
244 | resizePromises.push(imageGenerator.generateItunesIcon(this.options.icon));
245 | }
246 | if (this.android && this.options.splash) {
247 | resizePromises.push(
248 | imageGenerator.generatePlayStoreImage(this.options.splash)
249 | );
250 | }
251 |
252 | return Promise.all(resizePromises);
253 | }
254 | }
255 |
256 | module.exports = ResourcesGenerator;
257 |
--------------------------------------------------------------------------------
/generators/assets/templates/android/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | - <%= splashBackgroundColor %>
4 |
5 |
--------------------------------------------------------------------------------
/generators/assets/templates/android/launch_screen_bitmap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/generators/assets/templates/android/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/generators/assets/templates/ios/AppIconsetContents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "icon-20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "icon-20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "icon-29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "icon-29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "icon-29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "icon-40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "icon-40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "57x57",
47 | "idiom" : "iphone",
48 | "filename" : "icon-57@1x.png",
49 | "scale" : "1x"
50 | },
51 | {
52 | "size" : "57x57",
53 | "idiom" : "iphone",
54 | "filename" : "icon-57@2x.png",
55 | "scale" : "2x"
56 | },
57 | {
58 | "size" : "60x60",
59 | "idiom" : "iphone",
60 | "filename" : "icon-60@2x.png",
61 | "scale" : "2x"
62 | },
63 | {
64 | "size" : "60x60",
65 | "idiom" : "iphone",
66 | "filename" : "icon-60@3x.png",
67 | "scale" : "3x"
68 | },
69 | {
70 | "size" : "20x20",
71 | "idiom" : "ipad",
72 | "filename" : "icon-20@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "20x20",
77 | "idiom" : "ipad",
78 | "filename" : "icon-20@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "29x29",
83 | "idiom" : "ipad",
84 | "filename" : "icon-29@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "29x29",
89 | "idiom" : "ipad",
90 | "filename" : "icon-29@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "40x40",
95 | "idiom" : "ipad",
96 | "filename" : "icon-40@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "40x40",
101 | "idiom" : "ipad",
102 | "filename" : "icon-40@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "50x50",
107 | "idiom" : "ipad",
108 | "filename" : "icon-50@1x.png",
109 | "scale" : "1x"
110 | },
111 | {
112 | "size" : "50x50",
113 | "idiom" : "ipad",
114 | "filename" : "icon-50@2x.png",
115 | "scale" : "2x"
116 | },
117 | {
118 | "size" : "72x72",
119 | "idiom" : "ipad",
120 | "filename" : "icon-72@1x.png",
121 | "scale" : "1x"
122 | },
123 | {
124 | "size" : "72x72",
125 | "idiom" : "ipad",
126 | "filename" : "icon-72@2x.png",
127 | "scale" : "2x"
128 | },
129 | {
130 | "size" : "76x76",
131 | "idiom" : "ipad",
132 | "filename" : "icon-76@1x.png",
133 | "scale" : "1x"
134 | },
135 | {
136 | "size" : "76x76",
137 | "idiom" : "ipad",
138 | "filename" : "icon-76@2x.png",
139 | "scale" : "2x"
140 | },
141 | {
142 | "size" : "83.5x83.5",
143 | "idiom" : "ipad",
144 | "filename" : "icon-83.5@2x.png",
145 | "scale" : "2x"
146 | },
147 | {
148 | "size" : "1024x1024",
149 | "idiom" : "ios-marketing",
150 | "filename" : "icon-1024@1x.png",
151 | "scale" : "1x"
152 | }
153 | ],
154 | "info" : {
155 | "version" : 1,
156 | "author" : "xcode"
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/generators/assets/templates/ios/LaunchImageLaunchimageContents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "orientation": "portrait",
5 | "idiom": "iphone",
6 | "extent": "full-screen",
7 | "minimum-system-version": "11.0",
8 | "filename": "Default-Portrait-812h@3x.png",
9 | "subtype": "2436h",
10 | "scale": "3x"
11 | },
12 | {
13 | "orientation": "landscape",
14 | "idiom": "iphone",
15 | "extent": "full-screen",
16 | "filename": "Default-Landscape-812h@3x.png",
17 | "minimum-system-version": "11.0",
18 | "subtype": "2436h",
19 | "scale": "3x"
20 | },
21 | {
22 | "extent" : "full-screen",
23 | "idiom" : "iphone",
24 | "subtype" : "736h",
25 | "filename" : "Default-Portrait-736h@3x.png",
26 | "minimum-system-version" : "8.0",
27 | "orientation" : "portrait",
28 | "scale" : "3x"
29 | },
30 | {
31 | "extent" : "full-screen",
32 | "idiom" : "iphone",
33 | "subtype" : "736h",
34 | "filename" : "Default-Landscape-736h@3x.png",
35 | "minimum-system-version" : "8.0",
36 | "orientation" : "landscape",
37 | "scale" : "3x"
38 | },
39 | {
40 | "extent" : "full-screen",
41 | "idiom" : "iphone",
42 | "subtype" : "667h",
43 | "filename" : "Default-667h@2x.png",
44 | "minimum-system-version" : "8.0",
45 | "orientation" : "portrait",
46 | "scale" : "2x"
47 | },
48 | {
49 | "orientation" : "portrait",
50 | "idiom" : "iphone",
51 | "filename" : "Default@2x.png",
52 | "extent" : "full-screen",
53 | "minimum-system-version" : "7.0",
54 | "scale" : "2x"
55 | },
56 | {
57 | "extent" : "full-screen",
58 | "idiom" : "iphone",
59 | "subtype" : "retina4",
60 | "filename" : "Default-568h@2x.png",
61 | "minimum-system-version" : "7.0",
62 | "orientation" : "portrait",
63 | "scale" : "2x"
64 | },
65 | {
66 | "orientation" : "landscape",
67 | "idiom" : "ipad",
68 | "filename" : "Default-Landscape.png",
69 | "extent" : "full-screen",
70 | "minimum-system-version" : "7.0",
71 | "scale" : "1x"
72 | },
73 | {
74 | "orientation" : "landscape",
75 | "idiom" : "ipad",
76 | "filename" : "Default-Landscape@2x.png",
77 | "extent" : "full-screen",
78 | "minimum-system-version" : "7.0",
79 | "scale" : "2x"
80 | },
81 | {
82 | "orientation" : "portrait",
83 | "idiom" : "iphone",
84 | "extent" : "full-screen",
85 | "scale" : "1x"
86 | },
87 | {
88 | "orientation" : "portrait",
89 | "idiom" : "iphone",
90 | "extent" : "full-screen",
91 | "scale" : "2x"
92 | },
93 | {
94 | "orientation" : "portrait",
95 | "idiom" : "iphone",
96 | "extent" : "full-screen",
97 | "subtype" : "retina4",
98 | "scale" : "2x"
99 | },
100 | {
101 | "orientation" : "portrait",
102 | "idiom" : "ipad",
103 | "filename" : "Default-Portrait.png",
104 | "extent" : "full-screen",
105 | "scale" : "1x"
106 | },
107 | {
108 | "orientation" : "portrait",
109 | "idiom" : "ipad",
110 | "filename" : "Default-Portrait@2x.png",
111 | "extent" : "full-screen",
112 | "scale" : "2x"
113 | },
114 | {
115 | "orientation" : "portrait",
116 | "idiom" : "ipad",
117 | "filename" : "Default-Portrait.png",
118 | "extent" : "full-screen",
119 | "minimum-system-version" : "7.0",
120 | "scale" : "1x"
121 | },
122 | {
123 | "orientation" : "portrait",
124 | "idiom" : "ipad",
125 | "filename" : "Default-Portrait@2x.png",
126 | "extent" : "full-screen",
127 | "minimum-system-version" : "7.0",
128 | "scale" : "2x"
129 | },
130 | ],
131 | "info" : {
132 | "version" : 1,
133 | "author" : "xcode"
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/generators/base/README.md:
--------------------------------------------------------------------------------
1 | # Base Project setup
2 |
3 | *Tired of the same empty initial app? Start with a real React Native Project*
4 |
5 | Use `yo rn-toolbox:base` to replace the base project
6 |
7 | **Features**
8 | - Centralized App.js
9 | - [react-navigation](https://github.com/react-community/react-navigation) 2 pages setup
10 | - Custom header
11 | - Custom button
12 | - Centralized app style
13 | - Page container
14 |
--------------------------------------------------------------------------------
/generators/base/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const analytics = require('../../analytics');
3 |
4 | class BaseGenerator extends Base {
5 | initializing() {
6 | analytics.pageview('/base').send();
7 | this.composeWith('rn-toolbox:checkversion');
8 | }
9 |
10 | prompting() {
11 | const config = this.fs.readJSON(this.destinationPath('package.json'));
12 | return this.prompt([
13 | {
14 | type: 'input',
15 | name: 'appName',
16 | message: 'Your react native app directory name',
17 | default: config.name,
18 | },
19 | ]).then(answers => {
20 | this.answers = answers;
21 | });
22 | }
23 |
24 | install() {
25 | this.yarnInstall(
26 | [
27 | 'react-navigation',
28 | 'react-navigation-redux-helpers',
29 | 'redux',
30 | 'react-redux',
31 | ],
32 | { cwd: this.destinationRoot() }
33 | );
34 | }
35 |
36 | writing() {
37 | this.fs.delete(this.destinationPath('__tests__'));
38 | this.fs.delete(this.destinationPath('App.js'));
39 | this.fs.copyTpl(
40 | this.templatePath('**/*.js'),
41 | this.destinationPath(''),
42 | this.answers
43 | );
44 | }
45 |
46 | end() {
47 | this.config.set('base', true);
48 | this.config.save();
49 | }
50 | }
51 |
52 | module.exports = BaseGenerator;
53 |
--------------------------------------------------------------------------------
/generators/base/templates/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { AppRegistry } from 'react-native';
4 | import App from '<%= appName %>/src/App';
5 |
6 | AppRegistry.registerComponent('<%= appName %>', () => App);
7 |
--------------------------------------------------------------------------------
/generators/base/templates/src/App.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import { Provider } from 'react-redux';
5 |
6 | import createStore from '<%= appName %>/src/modules/store';
7 | import RootNavigation from './RootNavigation';
8 |
9 | const store = createStore();
10 |
11 | export default class App extends Component {
12 |
13 | render() {
14 | return (
15 |
16 |
17 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/generators/base/templates/src/RootNavigation.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import { connect } from 'react-redux';
5 |
6 | import { createStackNavigator } from 'react-navigation';
7 |
8 | import * as Pages from '<%= appName %>/src/pages';
9 | import { navListener } from '<%= appName %>/src/modules/Nav/module';
10 |
11 | export const AppNavigator = createStackNavigator({
12 | home: {
13 | screen: Pages.Home,
14 | },
15 | });
16 |
17 | class App extends React.Component {
18 | render() {
19 | return (
20 |
27 | );
28 | }
29 | }
30 |
31 | const mapStateToProps = state => ({
32 | nav: state.nav,
33 | });
34 |
35 | const AppWithNavigationState = connect(mapStateToProps)(App);
36 |
37 | export default AppWithNavigationState;
38 |
--------------------------------------------------------------------------------
/generators/base/templates/src/components/Page.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { PropTypes } from 'react';
4 | import { View, StyleSheet } from 'react-native';
5 | import theme from '<%= appName %>/src/theme';
6 |
7 | const styles = StyleSheet.create({
8 | page: {
9 | flex: 1,
10 | flexDirection: 'column',
11 | justifyContent: 'flex-start',
12 | },
13 | });
14 |
15 | export default (props: PropsType) => (
16 |
23 | {props.children}
24 |
25 | );
26 |
27 | type PropsType = {
28 | children: React$Element<*> | React$Element<*>[],
29 | noMargin: boolean,
30 | noNavBar: boolean,
31 | backgroundColor: string,
32 | };
33 |
--------------------------------------------------------------------------------
/generators/base/templates/src/components/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export { default as Page } from './Page';
4 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/App/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export * from './module';
4 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/App/module.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | const initialState = {};
4 |
5 | // ACTION TYPES
6 | export const actionTypes = {};
7 |
8 | // REDUCER
9 | export function appReducer(state = initialState, action) {
10 | switch (action.type) {
11 | default:
12 | return state;
13 | }
14 | }
15 |
16 | // ACTION CREATORS
17 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/Nav/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export * from './module';
4 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/Nav/module.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { AppNavigator } from '<%= appName %>/src/RootNavigation';
4 | import {
5 | createReduxBoundAddListener,
6 | createReactNavigationReduxMiddleware,
7 | } from 'react-navigation-redux-helpers';
8 |
9 | const initialNavState = AppNavigator.router.getStateForAction(
10 | AppNavigator.router.getActionForPathAndParams('home')
11 | );
12 |
13 | export const navReducer = (state = initialNavState, action) => {
14 | const nextState = AppNavigator.router.getStateForAction(action, state);
15 |
16 | // Simply return the original `state` if `nextState` is null or undefined.
17 | return nextState || state;
18 | };
19 |
20 | export const navMiddleware = createReactNavigationReduxMiddleware(
21 | 'root',
22 | state => state.nav
23 | );
24 |
25 | export const navListener = createReduxBoundAddListener('root');
26 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/rootEnhancer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { applyMiddleware } from 'redux';
4 | import { navMiddleware } from './Nav';
5 |
6 | const rootEnhancer = applyMiddleware(navMiddleware);
7 |
8 | export default rootEnhancer;
9 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/rootReducer.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { combineReducers } from 'redux';
4 |
5 | import { appReducer } from './App';
6 | import { navReducer } from './Nav';
7 |
8 | const rootReducer = combineReducers({
9 | app: appReducer,
10 | nav: navReducer,
11 | });
12 |
13 | export default rootReducer;
14 |
--------------------------------------------------------------------------------
/generators/base/templates/src/modules/store.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { createStore } from 'redux';
4 | import enhancer from '<%= appName %>/src/modules/rootEnhancer';
5 | import reducers from '<%= appName %>/src/modules/rootReducer';
6 |
7 | export default () => createStore(reducers, enhancer);
8 |
--------------------------------------------------------------------------------
/generators/base/templates/src/pages/Home/Home.component.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import React, { Component } from 'react';
4 | import { Platform, Text, View } from 'react-native';
5 |
6 | import { Page } from '<%= appName %>/src/components';
7 |
8 | import styles from './Home.style';
9 |
10 | const instructions = Platform.select({
11 | ios: 'Press Cmd+R to reload,\n' +
12 | 'Cmd+D or shake for dev menu',
13 | android: 'Double tap R on your keyboard to reload,\n' +
14 | 'Shake or press menu button for dev menu',
15 | });
16 |
17 | export default class Home extends Component {
18 | static navigationOptions = {
19 | title: 'Home',
20 | };
21 | props: PropsType;
22 |
23 | render() {
24 | return (
25 |
26 |
27 |
28 | Welcome to React Native!
29 |
30 |
31 | This is the Home page
32 |
33 |
34 | {instructions}
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | type PropsType = {
43 | navigation: any,
44 | };
45 |
--------------------------------------------------------------------------------
/generators/base/templates/src/pages/Home/Home.container.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { connect } from 'react-redux';
4 | import Home from './Home.component';
5 |
6 | const mapStateToProps = state => ({});
7 |
8 | const mapDispatchToProps = dispatch => ({});
9 |
10 | const HomeContainer = connect(mapStateToProps, mapDispatchToProps)(Home);
11 |
12 | export default HomeContainer;
13 |
--------------------------------------------------------------------------------
/generators/base/templates/src/pages/Home/Home.style.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import { StyleSheet, Text, View } from 'react-native';
4 | import theme from '<%= appName %>/src/theme';
5 |
6 | export default StyleSheet.create({
7 | container: {
8 | flex: 1,
9 | justifyContent: 'center',
10 | alignItems: 'center',
11 | },
12 | welcome: {
13 | ...theme.fonts.header,
14 | textAlign: 'center',
15 | margin: theme.grid.x1,
16 | },
17 | instructions: {
18 | ...theme.fonts.default,
19 | textAlign: 'center',
20 | color: theme.colors.darkGray,
21 | marginBottom: theme.grid.x1,
22 | },
23 | });
24 |
--------------------------------------------------------------------------------
/generators/base/templates/src/pages/Home/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export { default } from './Home.container';
4 |
--------------------------------------------------------------------------------
/generators/base/templates/src/pages/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | export { default as Home } from './Home';
4 |
--------------------------------------------------------------------------------
/generators/base/templates/src/theme/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | const gridSize = 8;
4 |
5 | export default {
6 | fonts: {
7 | header: {
8 | fontSize: 20,
9 | fontWeight: 'bold',
10 | },
11 | default: {
12 | fontSize: 16,
13 | }
14 | },
15 | colors: {
16 | oceanBlue: '#05A5D1',
17 | darkGray: '#333333',
18 | },
19 | grid: {
20 | x1: gridSize * 1,
21 | x2: gridSize * 2,
22 | x3: gridSize * 3,
23 | x4: gridSize * 4,
24 | x5: gridSize * 5,
25 | x6: gridSize * 6,
26 | x8: gridSize * 8,
27 | x9: gridSize * 9,
28 | x10: gridSize * 10,
29 | x12: gridSize * 12,
30 | x14: gridSize * 14,
31 | x16: gridSize * 16,
32 | },
33 | dimensions: {
34 | touchableHeight: 48,
35 | visibleButtonHeight: 36,
36 | },
37 | };
38 |
--------------------------------------------------------------------------------
/generators/bitrise/README.md:
--------------------------------------------------------------------------------
1 | # Bitrise setup for continuous deployment
2 |
3 | :warning: ***Having set up Fastlane with `yo rn-toolbox:fastlane-setup` is a requirement***
4 |
5 | This will:
6 | * upload your app to HockeyApp on every push to the `staging` branch
7 | * upload your app to TestFlight and optionally Android Beta on every push to the `master` branch
8 |
9 | ## 1/ Setup a deployment workflow
10 |
11 | ### Generate the bitrise.yml files
12 |
13 | Run anywhere:
14 | ```
15 | yo rn-toolbox:bitrise
16 | ```
17 |
18 | This will generate `bitrise/bitrise.yml`.
19 |
20 | Commit and push this bitrise.yml file.
21 |
22 | ## 2/ Create a new app on Bitrise
23 |
24 | ### Setting up SSH
25 |
26 | When asked *Do you need to use an additional private repository?*, select *I need to*.
27 | Add the ssh key to a Github account that has access to both your project repository and your match certificates repository.
28 |
29 | We recommend creating a bot/machine Github account for this. [See here](https://developer.github.com/guides/managing-deploy-keys/#machine-users) for more info on *machine user*.
30 |
31 | ### Project build configuration
32 |
33 | - Select *Other/Manual* and choose a Xamarin XCode build.
34 |
35 | ### Add secret environment variables
36 |
37 | - You need to provide the following secret variables in order for the build to run successfully
38 | - `MATCH_PASSWORD`
39 | - `GRADLE_KEYSTORE_PASSWORD` (Found in the *.env* file)
40 | - `GRADLE_KEYSTORE_ALIAS_PASSWORD` (Found in the *.env* file)
41 | - `FL_HOCKEY_API_TOKEN` (Found in the *.env* file)
42 |
43 | ### Setup Android Beta deployment
44 |
45 | If you want to deploy to Android Beta:
46 |
47 | 1. head over to the [Google Play Developers Console](https://play.google.com/apps/publish)
48 | 2. go to Settings > API Access
49 |
50 | If you already have a `Services accounts` section with an e-mail and a link to see the authorizations,
51 | skip to **9**. Else:
52 |
53 | 3. if needed, accept the terms and conditions
54 | 4. if needed, click the `Create a project` button
55 | 5. under `Services accounts`, click `Authorize access`
56 | 6. check :
57 | * Visibility
58 | * Edit store listing, pricing & distribution
59 | * Manage Production APKs
60 | * Manage Alpha & Beta APKs
61 | * Manage Alpha & Beta users
62 | 7. submit
63 | 8. come back to Settings > API Access
64 | 9. under `Services accounts`, click `Show in Google Developers Console`.
65 | 10. click `Create credentials` and `Service account key`.
66 | 11. select `Compute Engine default service account` and JSON
67 | 12. once you validate, your API key should be downloaded on your computer
68 |
69 | Now that you have your API key:
70 |
71 | 13. head over to Bitrise, edit your workflow, and go to the `Code signing & Files` section
72 | 14. at the bottom of the page, click `Add another File`
73 | 15. select `Generic file`
74 | 16. enter the unique id `GOOGLE_KEY`
75 | 17. upload the JSON key
76 | 18. save the workflow
77 |
78 | ## 3/ Enjoy :balloon:
79 |
80 | Sit back and relax, you can now enjoy your first deployment build!
81 |
82 | ## Testing
83 |
84 | This generator also contains a Test pipe but no trigger to run it. If you want you can add one but we strongly recommend using [TravisCI](../travisci/README.md) instead to run your tests.
85 |
--------------------------------------------------------------------------------
/generators/bitrise/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const analytics = require('../../analytics');
3 |
4 | class BitriseGenerator extends Base {
5 | initializing() {
6 | analytics.pageview('/bitrise').send();
7 | this.composeWith('rn-toolbox:checkversion');
8 | if (!this.config.get('fastlane')) {
9 | this.log.error('You need to run `yo rn-toolbox:fastlane` first.');
10 | }
11 | }
12 |
13 | prompting() {
14 | return this.prompt([
15 | {
16 | type: 'input',
17 | name: 'reactNativeDirectory',
18 | message:
19 | 'Path to the react native project relative to the root of the repository',
20 | required: true,
21 | default: '.',
22 | },
23 | {
24 | type: 'input',
25 | name: 'androidProdAppId',
26 | message:
27 | 'If you want to upload to Android Beta, enter your applicationId (ie. com.android.test).',
28 | },
29 | ]).then(answers => {
30 | this.answers = answers;
31 | });
32 | }
33 |
34 | writing() {
35 | this.fs.copyTpl(
36 | this.templatePath('bitrise.yml'),
37 | this.destinationPath('bitrise.yml'),
38 | this.answers
39 | );
40 | }
41 | }
42 |
43 | module.exports = BitriseGenerator;
44 |
--------------------------------------------------------------------------------
/generators/bitrise/templates/bitrise.yml:
--------------------------------------------------------------------------------
1 | ---
2 | format_version: 1.3.1
3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
4 | app:
5 | envs:
6 | - opts:
7 | is_expand: true
8 | FASTLANE_XCODE_LIST_TIMEOUT: '0'
9 | - opts:
10 | is_expand: true
11 | MATCH_READONLY: 'true'
12 | - opts:
13 | is_expand: true
14 | PRODUCE_SKIP_ITC: 'true'
15 | - opts:
16 | is_expand: true
17 | PRODUCE_SKIP_DEVCENTER: 'true'
18 | - opts:
19 | is_expand: true
20 | REACT_NATIVE_DIRECTORY: "./<%= reactNativeDirectory %>"
21 | workflows:
22 | HockeyApp:
23 | steps:
24 | - script@1.1.3:
25 | title: Set Gem home for cache purposes
26 | inputs:
27 | - content: |-
28 | #!/bin/bash
29 | pwd
30 | set -ev
31 | envman add --key GEM_HOME --value "$(gem environment gemdir)"
32 | - activate-ssh-key@3.1.1:
33 | title: Activate App SSH key
34 | inputs:
35 | - ssh_key_save_path: "$HOME/.ssh/steplib_ssh_step_id_rsa"
36 | - git-clone@3.4.1: {}
37 | - cache-pull@0.9.2:
38 | is_always_run: true
39 | - nvm@1.0.1:
40 | inputs:
41 | - node_version: 6.9.4
42 | - yarn@0.0.3:
43 | deps:
44 | brew:
45 | - name: yarn
46 | inputs:
47 | - workdir: "$BITRISE_SOURCE_DIR/$REACT_NATIVE_DIRECTORY"
48 | - command: ''
49 | - args: ''
50 | - yarn@0.0.3:
51 | inputs:
52 | - command: test
53 | - args: ''
54 | - script@1.1.3:
55 | title: Install bundles
56 | inputs:
57 | - content: |-
58 | #!/bin/bash
59 | bundle install
60 | - working_dir: "$REACT_NATIVE_DIRECTORY"
61 | - script@1.1.3:
62 | title: Deploy Android App to HockeyApp
63 | inputs:
64 | - content: |-
65 | #!/bin/bash
66 | bundle exec fastlane android deploy --env=staging
67 | - working_dir: "$REACT_NATIVE_DIRECTORY"
68 | - script@1.1.3:
69 | title: Deploy iOS to Hockey App
70 | inputs:
71 | - content: |-
72 | #!/bin/bash
73 | bundle exec fastlane ios deploy --env=staging
74 | - working_dir: "$REACT_NATIVE_DIRECTORY"
75 | - cache-push@0.9.4:
76 | inputs:
77 | - cache_paths: |-
78 | ./$REACT_NATIVE_DIRECTORY/node_modules -> ./$REACT_NATIVE_DIRECTORY/yarn.lock
79 | $HOME/.nvm -> $HOME/.nvm/package.json
80 | $HOME/.gradle
81 | ./$REACT_NATIVE_DIRECTORY/android/.gradle
82 | $GEM_HOME -> ./$REACT_NATIVE_DIRECTORY/Gemfile.lock
83 | ./$REACT_NATIVE_DIRECTORY/ios/Pods -> ./$REACT_NATIVE_DIRECTORY/ios/Podfile.lock
84 | jest/tmp
85 | - ignore_check_on_paths: |-
86 | $HOME/.gradle/*.lock
87 | $HOME/.gradle/*.bin
88 | $HOME/.gradle/*.log
89 |
90 | ./$REACT_NATIVE_DIRECTORY/android/.gradle/*.lock
91 | ./$REACT_NATIVE_DIRECTORY/android/.gradle/*.bin
92 | Pre-production:
93 | steps:
94 | - script@1.1.3:
95 | title: Set Gem home for cache purposes
96 | inputs:
97 | - content: |-
98 | #!/bin/bash
99 | pwd
100 | set -ev
101 | envman add --key GEM_HOME --value "$(gem environment gemdir)"
102 | - activate-ssh-key@3.1.1:
103 | title: Activate App SSH key
104 | inputs:
105 | - ssh_key_save_path: "$HOME/.ssh/steplib_ssh_step_id_rsa"
106 | - git-clone@3.4.1: {}
107 | - cache-pull@0.9.2:
108 | is_always_run: true
109 | - nvm@1.0.1:
110 | inputs:
111 | - node_version: 6.9.4
112 | - yarn@0.0.3:
113 | deps:
114 | brew:
115 | - name: yarn
116 | inputs:
117 | - workdir: "$BITRISE_SOURCE_DIR/$REACT_NATIVE_DIRECTORY"
118 | - command: ''
119 | - args: ''
120 | - yarn@0.0.3:
121 | inputs:
122 | - command: test
123 | - args: ''
124 | - script@1.1.3:
125 | title: Install bundles
126 | inputs:
127 | - content: |-
128 | #!/bin/bash
129 | bundle install
130 | - working_dir: "$REACT_NATIVE_DIRECTORY"<% if (androidProdAppId) { %>
131 | - script@1.1.3:
132 | title: Build Android App
133 | inputs:
134 | - content: |-
135 | #!/bin/bash
136 | bundle exec fastlane android deploy --env=prod
137 | - working_dir: "$REACT_NATIVE_DIRECTORY"
138 | - google-play-deploy@1.1.0:
139 | inputs:
140 | - service_account_json_key_path: "$BITRISEIO_GOOGLE_KEY_URL"
141 | - package_name: "<%= androidProdAppId %>"
142 | - apk_path: "$REACT_NATIVE_DIRECTORY/android/app/build/outputs/apk/app-release.apk"
143 | - track: beta
144 | - whatsnews_dir: ''
145 | - service_account_email: ''
146 | - key_file_path: ''<% } %>
147 | - script@1.1.3:
148 | title: Deploy iOS to TestFlight
149 | inputs:
150 | - content: |-
151 | #!/bin/bash
152 | bundle exec fastlane ios deploy --env=prod
153 | - working_dir: "$REACT_NATIVE_DIRECTORY"
154 | - cache-push@0.9.4:
155 | inputs:
156 | - cache_paths: |-
157 | ./$REACT_NATIVE_DIRECTORY/node_modules -> ./$REACT_NATIVE_DIRECTORY/yarn.lock
158 | $HOME/.nvm -> $HOME/.nvm/package.json
159 | $HOME/.gradle
160 | ./$REACT_NATIVE_DIRECTORY/android/.gradle
161 | $GEM_HOME -> ./$REACT_NATIVE_DIRECTORY/Gemfile.lock
162 | ./$REACT_NATIVE_DIRECTORY/ios/Pods -> ./$REACT_NATIVE_DIRECTORY/ios/Podfile.lock
163 | jest/tmp
164 | - ignore_check_on_paths: |-
165 | $HOME/.gradle/*.lock
166 | $HOME/.gradle/*.bin
167 | $HOME/.gradle/*.log
168 |
169 | ./$REACT_NATIVE_DIRECTORY/android/.gradle/*.lock
170 | ./$REACT_NATIVE_DIRECTORY/android/.gradle/*.bin
171 | Test:
172 | steps:
173 | - activate-ssh-key@3.1.1:
174 | title: Activate App SSH key
175 | inputs:
176 | - ssh_key_save_path: "$HOME/.ssh/steplib_ssh_step_id_rsa"
177 | - git-clone@3.4.1: {}
178 | - cache-pull@0.9.2:
179 | is_always_run: true
180 | - nvm@1.0.1:
181 | inputs:
182 | - node_version: 6.9.4
183 | - yarn@0.0.3:
184 | deps:
185 | brew:
186 | - name: yarn
187 | inputs:
188 | - workdir: "$BITRISE_SOURCE_DIR/$REACT_NATIVE_DIRECTORY"
189 | - command: ''
190 | - args: ''
191 | - yarn@0.0.3:
192 | inputs:
193 | - command: test
194 | - args: ''
195 | trigger_map:
196 | - pull_request_target_branch: staging
197 | workflow: Test
198 | - push_branch: staging
199 | workflow: HockeyApp
200 | - push_branch: master
201 | workflow: Pre-production
202 |
--------------------------------------------------------------------------------
/generators/checkversion/checkUpdate.js:
--------------------------------------------------------------------------------
1 | const http = require('http');
2 | const compareVersion = require('compare-version');
3 | const packageSettings = require('../../package.json');
4 |
5 | function fetchVersion() {
6 | return new Promise((resolve, reject) => {
7 | http.get(
8 | {
9 | host: 'registry.npmjs.org',
10 | path: '/generator-rn-toolbox',
11 | },
12 | response => {
13 | // Continuously update stream with data
14 | let body = '';
15 |
16 | response.on('error', reject);
17 |
18 | response.on('data', d => {
19 | body += d;
20 | });
21 |
22 | response.on('end', () => {
23 | try {
24 | resolve(JSON.parse(body));
25 | } catch (err) {
26 | reject(new Error('ERROR_ON_FETCH'));
27 | }
28 | });
29 | }
30 | );
31 | });
32 | }
33 |
34 | function getVersion() {
35 | return fetchVersion().then(res => res['dist-tags'].latest);
36 | }
37 |
38 | function isPackageUpdated() {
39 | return getVersion().then(
40 | latestVersion => compareVersion(packageSettings.version, latestVersion) >= 0
41 | );
42 | }
43 |
44 | module.exports = isPackageUpdated;
45 |
--------------------------------------------------------------------------------
/generators/checkversion/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | require('colors');
3 | const checkVersion = require('./checkUpdate');
4 |
5 | class CheckVersionGenerator extends Base {
6 | initializing() {
7 | return checkVersion()
8 | .then(isUpdated => {
9 | this.log(
10 | '🌟🌟🌟 ️ ' +
11 | 'Deprecation Notice'.bold.bgYellow.black +
12 | ' 🌟🌟🌟\n' +
13 | 'generator-rn-toolbox is being deprecated in favor of @bam.tech/react-native-make.\nYou can find out more here: '
14 | .yellow +
15 | 'https://github.com/bamlab/react-native-make'.bold.yellow
16 | );
17 | if (!isUpdated)
18 | this.log.error(
19 | "You do not have the latest version of this generator\nIt is recommended to update it using 'npm i -g generator-rn-toolbox'"
20 | .bgRed.white.bold
21 | );
22 | })
23 | .catch(() => {
24 | this.log.error(
25 | 'Unable to check for updates please check your network'.bgRed.white
26 | .bold
27 | );
28 | });
29 | }
30 | }
31 |
32 | module.exports = CheckVersionGenerator;
33 |
--------------------------------------------------------------------------------
/generators/circleci/README.md:
--------------------------------------------------------------------------------
1 | # CircleCI setup for continuous integration
2 |
3 | CircleCI is a widely used CI service for both web and mobile projects. After running this generator, pushing your code to CircleCI should result in a succesful build configured with dependency caching and good practices.
4 |
--------------------------------------------------------------------------------
/generators/circleci/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | require('colors');
3 | const glob = require('glob');
4 | const analytics = require('../../analytics');
5 |
6 | // Command creators
7 | const getPassphraseAliasForEnvironment = environment =>
8 | `${environment.envName.toUpperCase()}_SECRETS_PASSPHRASE`;
9 | const getUnpackCommandForEnvironment = environment =>
10 | `sudo yarn unpack-secrets -e ${
11 | environment.envName
12 | } -p \${${getPassphraseAliasForEnvironment(environment)}}`;
13 | const getCodePushCommandForEnvironment = environment =>
14 | `yarn deploy -t soft -e ${environment.envName}`;
15 | const getAndroidHardDeployCommandForEnvironment = environment =>
16 | `yarn deploy -t hard -o android -e ${environment.envName}`;
17 | const getIosHardDeployCommandForEnvironment = environment =>
18 | `yarn deploy -t hard -o ios -e ${environment.envName}`;
19 |
20 | class CircleGenerator extends Base {
21 | initializing() {
22 | analytics.pageview('/circleci').send();
23 | this.composeWith('rn-toolbox:checkversion');
24 | if (!this.config.get('fastlane'))
25 | this.log.error(
26 | 'You need to run `yo rn-toolbox:fastlane` first.'.red.bold
27 | );
28 |
29 | if (!this.config.get('circleci-ready'))
30 | this.log.error(
31 | 'You need to have the deployment script and secrets archive from fastlane-setup to use this generator. Get them by running yo rn-toolbox:fastlane-setup.'
32 | .red.bold
33 | );
34 | }
35 |
36 | prompting() {
37 | const config = this.fs.readJSON(this.destinationPath('package.json'));
38 | const envFilepaths = glob.sync('fastlane/.env.*', {
39 | ignore: 'fastlane/.env.*.*',
40 | });
41 | const environments = envFilepaths.map(filePath => {
42 | const split = filePath.split('/');
43 | const envName = split[split.length - 1].split('.')[2];
44 | const fileString = this.fs.read(filePath);
45 | const envGitBranch = fileString
46 | .split("REPO_GIT_BRANCH='")[1]
47 | .split("'")[0];
48 | return {
49 | envName,
50 | envGitBranch,
51 | };
52 | });
53 | if (environments.length === 0)
54 | this.log.error(
55 | 'You need at least one environment setup with fastlane-env to run this generator. Run yo rn-toolbox:fastlane-env.'
56 | .red.bold
57 | );
58 |
59 | const prompts = [
60 | {
61 | type: 'input',
62 | name: 'projectName',
63 | message:
64 | 'Please confirm the react-native project name (as in react-native-init )',
65 | required: true,
66 | default: config.name,
67 | },
68 | {
69 | type: 'input',
70 | name: 'reactNativeDirectory',
71 | message:
72 | 'Path to the React Native project relative to the root of the repository (no trailing slash)',
73 | required: true,
74 | default: '.',
75 | },
76 | ];
77 |
78 | return this.prompt(prompts).then(answers => {
79 | this.answers = answers;
80 | this.environments = environments;
81 | });
82 | }
83 |
84 | writing() {
85 | // Command creators
86 | const numberOfEnvironments = this.environments.length;
87 | const getPerEnvironmentCommand = commandGetter => {
88 | let switchString = '';
89 | this.environments.forEach((environment, index) => {
90 | const prefix = index === 0 ? 'if' : 'elif';
91 | const suffix = index === numberOfEnvironments - 1 ? 'fi' : '';
92 | if (index > 0) switchString += '';
93 | switchString += `${prefix} [ "\${CIRCLE_BRANCH}" == "${
94 | environment.envGitBranch
95 | }" ];
96 | then
97 | ${commandGetter(environment)}
98 | ${suffix}`;
99 | });
100 | return switchString;
101 | };
102 | const getForAllEnvironmentsCommand = command => {
103 | let ifStatement = `if [ "\${CIRCLE_BRANCH}" == "${
104 | this.environments[0].envGitBranch
105 | }" ]`;
106 | this.environments.slice(1).forEach(environment => {
107 | ifStatement += `|| [ "\${CIRCLE_BRANCH}" == "${environment.envName}" ]`;
108 | });
109 | ifStatement += ';';
110 |
111 | return `${ifStatement}
112 | then
113 | ${command}
114 | fi`;
115 | };
116 |
117 | // Commands
118 | const unpackSecretsCommand = getPerEnvironmentCommand(
119 | getUnpackCommandForEnvironment
120 | );
121 | const installAppcenterCommand = getForAllEnvironmentsCommand(`echo 'export PATH=$(yarn global bin):$PATH' >> $BASH_ENV
122 | source $BASH_ENV
123 | yarn global add appcenter-cli
124 | appcenter login --token \${FL_APPCENTER_API_TOKEN} --quiet`);
125 | const codepushCommand = getPerEnvironmentCommand(
126 | getCodePushCommandForEnvironment
127 | );
128 | const androidHardDeployCommand = getPerEnvironmentCommand(
129 | getAndroidHardDeployCommandForEnvironment
130 | );
131 | const iosHardDeployCommand = getPerEnvironmentCommand(
132 | getIosHardDeployCommandForEnvironment
133 | );
134 | let branchesOnlyCommand = '';
135 | this.environments.forEach(environment => {
136 | branchesOnlyCommand += `
137 | - ${environment.envGitBranch}`;
138 | });
139 |
140 | // Create final config.yml file :-)
141 | this.fs.copyTpl(
142 | this.templatePath('config.yml'),
143 | this.destinationPath('.circleci/config.yml'),
144 | {
145 | ...this.answers,
146 | prefixRN: path => `${this.answers.reactNativeDirectory}/${path || ''}`,
147 | unpackSecretsCommand,
148 | installAppcenterCommand,
149 | codepushCommand,
150 | androidHardDeployCommand,
151 | iosHardDeployCommand,
152 | branchesOnlyCommand,
153 | }
154 | );
155 | }
156 |
157 | end() {
158 | this.log(
159 | `Custom config.yml created. Re-run yo rn-toolbox:circleci anytime to re-generate the file with latest environments information.`
160 | .green.bold
161 | );
162 | this.log(
163 | `Please make sure that CircleCI has access to your MATCH repo using 'Checkout ssh Keys' section in settings. Good practice: Github account should have readonly access and only to this repo.`
164 | .magenta.bold
165 | );
166 | this.log(
167 | `Please make sure that all of the following environment variables have been added in the Circle-CI console's Environment Variables section:`
168 | .magenta.bold
169 | );
170 | this.log(
171 | [
172 | 'FL_APPCENTER_API_TOKEN',
173 | 'MATCH_PASSWORD',
174 | 'FASTLANE_PASSWORD (<- password of AppstoreConnect Apple ID)',
175 | ]
176 | .concat(this.environments.map(getPassphraseAliasForEnvironment))
177 | .join(', ').magenta.bold
178 | );
179 | this.config.set('circleci', true);
180 | this.config.save();
181 | }
182 | }
183 |
184 | module.exports = CircleGenerator;
185 |
--------------------------------------------------------------------------------
/generators/circleci/templates/config.yml:
--------------------------------------------------------------------------------
1 | # prettier: disable
2 |
3 | version: 2
4 | jobs:
5 | node:
6 | working_directory: ~/<%= projectName %>
7 | docker:
8 | - image: circleci/android:api-28-node
9 | steps:
10 | - checkout
11 |
12 | - restore_cache:
13 | key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}
14 |
15 | - restore_cache:
16 | key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}
17 |
18 | - run:
19 | name: yarn install
20 | command: |
21 | cd <%= prefixRN() %>
22 | yarn
23 |
24 | - run:
25 | name: jest tests
26 | command: |
27 | cd <%= prefixRN() %>
28 | mkdir -p test-results/jest
29 | yarn test --maxWorkers=2
30 | environment:
31 | JEST_JUNIT_OUTPUT: <%= prefixRN('test-results/jest/junit.xml') %>
32 |
33 | - run:
34 | name: Unpack secrets
35 | command: |
36 | cd <%= prefixRN() %>
37 | <%- unpackSecretsCommand %>
38 |
39 | - restore_cache:
40 | key: bundle-v1-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}
41 |
42 | - run: cd <%= prefixRN() %> && sudo gem install bundler && bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
43 |
44 | - deploy:
45 | name: Deploy with CodePush
46 | command: |
47 | cd <%= prefixRN() %>
48 | <%- installAppcenterCommand %>
49 | <%- codepushCommand %>
50 |
51 | - save_cache:
52 | key: bundle-v1-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}
53 | paths:
54 | - ~/.bundle/
55 | - <%= prefixRN('vendor/bundle') %>
56 |
57 | - save_cache:
58 | key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}
59 | paths:
60 | - ~/.cache/yarn
61 |
62 | - save_cache:
63 | key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}
64 | paths:
65 | - <%= prefixRN('node_modules') %>
66 |
67 | - persist_to_workspace:
68 | root: ~/<%= projectName %>
69 | paths:
70 | - <%= prefixRN('node_modules') %>
71 | - <%= prefixRN('vendor/bundle') %>
72 |
73 | - store_test_results:
74 | path: <%= prefixRN('test-results') %>
75 |
76 | - store_artifacts:
77 | path: <%= prefixRN('test-results') %>
78 |
79 | android:
80 | working_directory: ~/<%= projectName %>
81 | docker:
82 | - image: circleci/android:api-28-node
83 | steps:
84 | - checkout:
85 | path: ~/<%= projectName %>
86 |
87 | - attach_workspace:
88 | at: ~/<%= projectName %>
89 |
90 | - run:
91 | name: Unpack secrets
92 | command: |
93 | cd <%= prefixRN() %>
94 | <%- unpackSecretsCommand %>
95 |
96 | - run: cd <%= prefixRN() %> && sudo gem install bundler && bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
97 |
98 | - run:
99 | name: yarn install
100 | command: |
101 | cd <%= prefixRN() %>
102 | yarn
103 |
104 | - deploy:
105 | name: Build, Sign & Deploy
106 | command: |
107 | cd <%= prefixRN() %>
108 | <%- androidHardDeployCommand %>
109 |
110 | - store_test_results:
111 | path: <%= prefixRN('test-results') %>
112 |
113 | - store_artifacts:
114 | path: <%= prefixRN('test-results') %>
115 |
116 | ios:
117 | macos:
118 | xcode: '10.1.0'
119 | environment:
120 | LC_ALL: en_US.UTF-8
121 | LANG: en_US.UTF-8
122 | working_directory: ~/<%= projectName %>
123 |
124 | # use a --login shell so our "set Ruby version" command gets picked up for later steps
125 | shell: /bin/bash --login -o pipefail
126 |
127 | steps:
128 | - checkout
129 |
130 | - run:
131 | name: set Ruby version
132 | command: echo "ruby-2.4" > ~/.ruby-version
133 |
134 | - run:
135 | name: Install brew dependencies
136 | command: HOMEBREW_NO_AUTO_UPDATE=1 brew install gpg
137 |
138 | - run:
139 | name: Unpack secrets
140 | command: |
141 | cd <%= prefixRN() %>
142 | <%- unpackSecretsCommand %>
143 |
144 | - restore_cache:
145 | key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}
146 |
147 | - restore_cache:
148 | key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}
149 |
150 | # not using a workspace here as Node and Yarn versions
151 | # differ between the macOS executor image and the Docker containers above
152 | - run:
153 | name: yarn install
154 | command: |
155 | cd <%= prefixRN() %>
156 | yarn
157 |
158 | - save_cache:
159 | key: yarn-v1-{{ checksum "<%= prefixRN('yarn.lock') %>" }}-{{ arch }}
160 | paths:
161 | - ~/.cache/yarn
162 |
163 | - save_cache:
164 | key: node-v1-{{ checksum "<%= prefixRN('package.json') %>" }}-{{ arch }}
165 | paths:
166 | - <%= prefixRN('node_modules') %>
167 |
168 | - restore_cache:
169 | key: bundle-v2-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}
170 |
171 | - run:
172 | command: cd <%= prefixRN() %> && sudo gem install bundler && bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
173 |
174 | - save_cache:
175 | key: bundle-v2-{{ checksum "<%= prefixRN('Gemfile.lock') %>" }}-{{ arch }}
176 | paths:
177 | - ~/.bundle/
178 | - /Users/distiller/.gem/ruby/2.4.5/
179 | - <%= prefixRN('vendor/bundle') %>
180 |
181 | - deploy:
182 | name: Match, Build, Sign & Deploy
183 | command: |
184 | cd <%= prefixRN() %>
185 | sudo xcode-select -s /Applications/Xcode-10.1.app
186 | <%- iosHardDeployCommand %>
187 |
188 | workflows:
189 | version: 2
190 | node-android-ios:
191 | jobs:
192 | - node
193 | - ios:
194 | requires:
195 | - node
196 | filters:
197 | branches:
198 | only: <%- branchesOnlyCommand %>
199 | - android:
200 | requires:
201 | - node
202 | filters:
203 | branches:
204 | only: <%- branchesOnlyCommand %>
205 |
--------------------------------------------------------------------------------
/generators/fastlane-env/README.md:
--------------------------------------------------------------------------------
1 | # Fastlane Environment Generator
2 |
3 | ## Usage
4 |
5 | ### 1. Create a new Environment
6 | ```
7 | yo rn-toolbox:fastlane-env
8 | ```
9 |
10 | ### 2. Generate the provisioning profiles
11 | ```
12 | bundle exec fastlane ios setup --env=
13 | ```
14 |
15 | ### 3. AppStore Setup (Appstore Deployment only)
16 |
17 | > :warning: AppStore deployment will require [app icons](../assets/README.md) to be setup
18 |
19 | - Right now iTunes Connect automatic app creation has been disabled due to issues and will necessitate manual setup
20 | - Head to [iTunes Connect](https://itunesconnect.apple.com/)
21 | - Create a new app using the app id you provided
22 |
23 | ### 4. Visual Studio App Center Setup (AppCenter Deployment only)
24 |
25 | App Center is the successor of HockeyApp.
26 |
27 | - Create an account on mobile.azure.com
28 | - Get your username at the bottom left of the welcome screen
29 | - Follow the steps to get a working [API Token](https://docs.microsoft.com/en-us/appcenter/api-docs/)
30 | - Choose `AppCenter` as deployment method in this generator
31 |
32 | ### 5. Deploy your app
33 | ***:warning: Automatic Play Store deployment is not yet available (PR welcome!)***
34 | ```
35 | bundle exec fastlane ios deploy --env=
36 | bundle exec fastlane android deploy --env=
37 | ```
38 |
39 | ## More
40 |
41 | > ### Centralizing environment variables
42 | >
43 | > You might want to share some environment variables accross multiple environments. In order to do so, simply create a `.env` file, remove the variable you want to share in all the environment files and add it in the `.env` one.
44 | >
45 | > On setup, only version names are centralized in the `.env` file
46 |
47 | > ### Adding a new device to the iOS provisioning profiles
48 | >
49 | > The `ios setup --env=` task mentionned above has been setup to allow you to regenerate your certificates in case you add a new device. Just run it again to include the new devices.
50 |
51 | ## Troubleshooting
52 |
53 | > ### **Setup or build error** *"Cloning GitHub repo"* takes too long:
54 | >
55 | > If you have never initiated connection with GitHub and it is not yet trusted. Enter `ssh -T git@github.com` and type 'yes' when you are asked if you trust Github.com
56 |
57 | > ### **Build error** *"The provisioning profile doesn't match the bundle ID"*:
58 | >
59 | > PRODUCT_BUNDLE_IDENTIFIER is probably specified for release in your pbxproj file and fastlane can't override it. Delete the line from your pbxproj file.
60 |
--------------------------------------------------------------------------------
/generators/fastlane-env/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const randomString = require('randomstring');
3 | const analytics = require('../../analytics');
4 | require('colors');
5 |
6 | const deploymentPlatforms = [
7 | { name: 'AppCenter', value: 'appcenter' },
8 | { name: 'AppStore', value: 'appstore' },
9 | { name: 'HockeyApp (deprecated)', value: 'hockeyapp' },
10 | ];
11 | const certificatesTypes = [
12 | { name: 'Adhoc', value: 'adhoc' },
13 | { name: 'In House (Enterprise only)', value: 'enterprise' },
14 | ];
15 | const cocoaPodsInstallCommands = [
16 | { commandName: 'sudo', args: ['gem', 'install', 'cocoapods'] },
17 | { commandName: 'pod', args: ['repo', 'update'], options: { cwd: 'ios' } },
18 | ];
19 | const reactNativeLinkAppcenterCommands = [
20 | { commandName: 'react-native', args: ['link', 'appcenter'] },
21 | { commandName: 'react-native', args: ['link', 'appcenter-analytics'] },
22 | { commandName: 'react-native', args: ['link', 'appcenter-crashes'] },
23 | ];
24 | const reactNativeLinkCodepushCommands = [
25 | { commandName: 'react-native', args: ['link', 'react-native-code-push'] },
26 | ];
27 | const installAppCenterFastlanePluginCommands = [
28 | { commandName: 'fastlane', args: ['add_plugin', 'appcenter'] },
29 | ];
30 | const installLoadJsonFastlanePluginCommands = [
31 | { commandName: 'fastlane', args: ['add_plugin', 'load_json'] },
32 | ];
33 | const installNokogiriCommands = [
34 | { commandName: 'bundle', args: ['add', 'nokogiri'] },
35 | ];
36 | const installGpgCommands = [{ commandName: 'brew', args: ['install', 'gpg'] }];
37 |
38 | class FastlaneEnvGenerator extends Base {
39 | initializing() {
40 | analytics.pageview('/fastlane-env').send();
41 | this.composeWith('rn-toolbox:checkversion');
42 | if (!this.config.get('fastlane')) {
43 | this.log.error(
44 | 'Running fastlane setup is required to use the environments. You can run it with yo rn-toolbox:fastlane-setup'
45 | .red.bold
46 | );
47 | }
48 | }
49 |
50 | prompting() {
51 | const config = this.fs.readJSON(this.destinationPath('package.json'));
52 | return this.prompt([
53 | {
54 | type: 'input',
55 | name: 'projectName',
56 | message:
57 | 'Please confirm the react-native project name (as in react-native-init )',
58 | default: config.name,
59 | },
60 | {
61 | type: 'input',
62 | name: 'environmentName',
63 | message: 'The name for this new environment (lowercase, no space)',
64 | default: 'myenv',
65 | },
66 | {
67 | type: 'input',
68 | name: 'repoGitBranch',
69 | message:
70 | 'The name of your repository Git branch for the environment just set',
71 | default: answers => answers.environmentName,
72 | },
73 | {
74 | type: 'input',
75 | name: 'companyName',
76 | message:
77 | 'The name of the company which will be publishing this application (used to generate android Keystore)',
78 | default: 'My Company',
79 | },
80 | {
81 | type: 'list',
82 | name: 'deploymentPlatform',
83 | message: 'Which platform will you use for deployment?',
84 | choices: deploymentPlatforms,
85 | },
86 | {
87 | type: 'input',
88 | name: 'appName',
89 | message: 'The app name for this environment',
90 | default: 'My App',
91 | },
92 | {
93 | type: 'input',
94 | name: 'appId',
95 | message: 'The App Id for this environment',
96 | default: answers =>
97 | `com.${answers.companyName.toLowerCase().replace(' ', '')}.${
98 | answers.projectName
99 | }.${answers.environmentName}`,
100 | },
101 | {
102 | type: 'list',
103 | name: 'certificateType',
104 | message: 'The type of certificate you will be using',
105 | choices: certificatesTypes,
106 | when: answers =>
107 | ['hockeyapp', 'appcenter'].includes(answers.deploymentPlatform),
108 | },
109 | {
110 | type: 'input',
111 | name: 'matchGit',
112 | message: 'Your git repo for match',
113 | default: 'git@github.com:mycompany/certificates.git',
114 | },
115 | {
116 | type: 'input',
117 | name: 'matchBranch',
118 | message: 'The branch you want to use for match',
119 | default: 'master',
120 | },
121 | {
122 | type: 'input',
123 | name: 'appleTeamId',
124 | message: 'The developer.apple.com team id for the certificates',
125 | default: 'XXXXXXXXXX',
126 | },
127 | {
128 | type: 'input',
129 | name: 'itunesTeamName',
130 | message: 'The appstoreconnect.apple.com team name',
131 | default: 'MyCompany',
132 | when: answers => answers.deploymentPlatform === 'appstore',
133 | },
134 | {
135 | type: 'input',
136 | name: 'appstoreConnectAppleId',
137 | message:
138 | 'An AppstoreConnect Apple Id (good practice: ID should have "developer" access - only allowed to upload builds). Can be entered later in fastlane/env.',
139 | when: answers => answers.deploymentPlatform === 'appstore',
140 | },
141 | {
142 | type: 'input',
143 | name: 'appleId',
144 | message:
145 | 'Your apple id (should be admin on the Apple Developer Portal)',
146 | default: 'dev@mycompany.com',
147 | },
148 | {
149 | type: 'input',
150 | name: 'keystorePassword',
151 | message: 'Your keystore password',
152 | default: randomString.generate(),
153 | validate: input => {
154 | if (input.includes(' ')) return 'No whitespace allowed';
155 | return true;
156 | },
157 | },
158 | {
159 | type: 'input',
160 | name: 'hockeyAppToken',
161 | message: 'A valid HockeyApp token',
162 | when: answers => answers.deploymentPlatform === 'hockeyapp',
163 | },
164 |
165 | {
166 | type: 'input',
167 | name: 'androidPlayStoreJsonKeyPath',
168 | message:
169 | 'A Google Play JSON Key relative path. Can be entered later in fastlane/env.',
170 | when: answers => answers.deploymentPlatform === 'appstore',
171 | },
172 | {
173 | type: 'confirm',
174 | name: 'useCodePush',
175 | message: 'Will you deploy with Appcenter CodePush on this environment?',
176 | },
177 | {
178 | type: 'input',
179 | name: 'appCenterUsername',
180 | message: 'A valid App Center Username',
181 | when: answers =>
182 | answers.deploymentPlatform === 'appcenter' || answers.useCodePush,
183 | },
184 | {
185 | type: 'input',
186 | name: 'appCenterToken',
187 | message: 'A valid App Center API token',
188 | when: answers =>
189 | answers.deploymentPlatform === 'appcenter' || answers.useCodePush,
190 | },
191 | {
192 | type: 'input',
193 | name: 'iosAppCenterId',
194 | message:
195 | 'The iOS project id on AppCenter for this environment, should be different than Android and not contain spaces',
196 | default: answers =>
197 | `${answers.appName.replace(/ /g, '')}-ios-${answers.environmentName}`,
198 | when: answers =>
199 | answers.deploymentPlatform === 'appcenter' || answers.useCodePush,
200 | },
201 | {
202 | type: 'input',
203 | name: 'androidAppCenterId',
204 | message:
205 | 'The Android project id on AppCenter for this environment, should be different than iOS and not contain spaces',
206 | default: answers =>
207 | `${answers.appName.replace(/ /g, '')}-android-${
208 | answers.environmentName
209 | }`,
210 | when: answers =>
211 | answers.deploymentPlatform === 'appcenter' || answers.useCodePush,
212 | },
213 | {
214 | type: 'input',
215 | name: 'iosCodePushDeploymentKey',
216 | message:
217 | "Your iOS CodePush deployment key (can be entered later in fastlane/env..secret if the App and Deployments don't exist yet in Appcenter)",
218 | when: answers => answers.useCodePush,
219 | },
220 | {
221 | type: 'input',
222 | name: 'androidCodePushDeploymentKey',
223 | message:
224 | "Your Android CodePush deployment key (can be entered later in fastlane/env..secret if the App and Deployments don't exist yet in Appcenter)",
225 | when: answers => answers.useCodePush,
226 | },
227 | {
228 | type: 'input',
229 | name: 'iosCodePushDeploymentName',
230 | message:
231 | "Your iOS CodePush deployment name (can be entered later in fastlane/env. if the App and Deployments don't exist yet in Appcenter)",
232 | when: answers => answers.useCodePush,
233 | },
234 | {
235 | type: 'input',
236 | name: 'androidCodePushDeploymentName',
237 | message:
238 | "Your Android CodePush deployment name (can be entered later in fastlane/env. if the App and Deployments don't exist yet in Appcenter)",
239 | when: answers => answers.useCodePush,
240 | },
241 | {
242 | type: 'confirm',
243 | name: 'useAppcenterSDK',
244 | message:
245 | 'Will you be using Appcenter Analytics and Crash reporting on this environment?',
246 | },
247 | {
248 | type: 'input',
249 | name: 'iosAppcenterAppSecret',
250 | message:
251 | "Your AppCenter app secret for the ios App (can be entered later in fastlane/env. if the App doesn't exist yet in Appcenter)",
252 | when: answers => answers.useAppcenterSDK,
253 | },
254 | {
255 | type: 'input',
256 | name: 'androidAppcenterAppSecret',
257 | message:
258 | "Your AppCenter app secret for the Android App (can be entered later in fastlane/env. if the app doesn't exist yet in Appcenter)",
259 | when: answers => answers.useAppcenterSDK,
260 | },
261 | ]).then(answers => {
262 | this.answers = answers;
263 | this.answers.lowerCaseProjectName = answers.projectName.toLowerCase();
264 | // Default certificateType to appstore if platform is appstore
265 | if (this.answers.deploymentPlatform === 'appstore') {
266 | this.answers.certificateType = 'appstore';
267 | }
268 | });
269 | }
270 |
271 | writing() {
272 | this.fs.copyTpl(
273 | this.templatePath('fastlane/env'),
274 | this.destinationPath(`fastlane/.env.${this.answers.environmentName}`),
275 | this.answers
276 | );
277 | this.fs.copyTpl(
278 | this.templatePath('fastlane/env.secret'),
279 | this.destinationPath(
280 | `fastlane/.env.${this.answers.environmentName}.secret`
281 | ),
282 | this.answers
283 | );
284 | this.fs.copyTpl(
285 | this.templatePath('environment/index.js'),
286 | this.destinationPath(
287 | `src/environment/index.${this.answers.environmentName}.js`
288 | ),
289 | this.answers
290 | );
291 | }
292 |
293 | install() {
294 | // Create Keystore file
295 | this._createKeystore();
296 |
297 | // Install AppCenter Fastlane Plugin
298 | if (this.answers.deploymentPlatform === 'appcenter') {
299 | this._runInstallCommandsWithErrorMessages(
300 | installAppCenterFastlanePluginCommands,
301 | 'the App Center Fastlane plugin',
302 | 'fastlane add_plugin appcenter'
303 | );
304 | }
305 |
306 | // Install CodePush npm library
307 | if (this.answers.useCodePush) {
308 | this.yarnInstall(['react-native-code-push'], {
309 | cwd: this.destinationRoot(),
310 | });
311 | this._runInstallCommandsWithErrorMessages(
312 | installNokogiriCommands,
313 | 'Nokogiri (xml manipulation)',
314 | 'bundle add nokogiri'
315 | );
316 | this.log(
317 | 'Appcenter CodePush configuration was added to your project for this environment.'
318 | );
319 | }
320 |
321 | // Install App Center npm libraries
322 | if (this.answers.useAppcenterSDK) {
323 | this.yarnInstall(
324 | ['appcenter', 'appcenter-analytics', 'appcenter-crashes'],
325 | { cwd: this.destinationRoot() }
326 | );
327 | this._runInstallCommandsWithErrorMessages(
328 | installLoadJsonFastlanePluginCommands,
329 | 'the load_json Fastlane plugin',
330 | 'fastlane add_plugin load_json'
331 | );
332 | this.log(
333 | 'Appcenter Analytics and Crash reporting configuration were added to your project for this environment.'
334 | );
335 | }
336 |
337 | // Install gpg if secrets archive is needed
338 | if (this.answers.useSecretsArchive) {
339 | this._runInstallCommandsWithErrorMessages(
340 | installGpgCommands,
341 | 'gpg',
342 | 'brew install gpg'
343 | );
344 | this.log('GPG was added in order to encrypt the secrets archive.');
345 | }
346 | }
347 |
348 | end() {
349 | this._reactNativeLinkDependencies();
350 | if (this.answers.useCodePush) {
351 | this.log(
352 | 'CodePush has been linked but you might need to remove it from PodFile and link the binary manually in Xcode for it to work properly on iOS (see CodePush docs).'
353 | .magenta.bold
354 | );
355 | this.log(
356 | 'CodePush config has been added but you still need to wrap your js entry point with the library + config (see CodePush docs).'
357 | .green.bold
358 | );
359 | }
360 | this.log('Environment has been created, please run'.green);
361 | this.log(
362 | `bundle exec fastlane ios setup --env=${this.answers.environmentName}`
363 | .green.bold
364 | );
365 | this.log('to create the provisioning profiles'.green);
366 | }
367 |
368 | _reactNativeLinkDependencies() {
369 | // Link AppCenter CodePush
370 | if (this.answers.useCodePush) {
371 | this.log(
372 | 'Linking AppCenter CodePush for you...\nPLEASE LEAVE THE KEY FIELDS BLANK.'
373 | .magenta.bold
374 | );
375 | this._runInstallCommandsWithErrorMessages(
376 | reactNativeLinkCodepushCommands,
377 | 'AppCenter CodePush',
378 | 'react-native link react-native-code-push'
379 | );
380 | }
381 |
382 | if (this.answers.useAppcenterSDK) {
383 | this.log('Installing CocoaPods (please provide sudo password)...');
384 | this._runInstallCommandsWithErrorMessages(
385 | cocoaPodsInstallCommands,
386 | 'CocoaPods',
387 | 'sudo gem install cocoapods && cd ios && pod init && pod repo update.'
388 | );
389 | }
390 |
391 | // Link AppCenter SDK
392 | if (this.answers.useAppcenterSDK) {
393 | this.log(
394 | 'Linking AppCenter SDK for you...\nPLEASE LEAVE THE "SECRETS" FIELDS BLANK.'
395 | .magenta.bold
396 | );
397 | this._runInstallCommandsWithErrorMessages(
398 | reactNativeLinkAppcenterCommands,
399 | 'AppCenter SDK libraries',
400 | 'react-native link appcenter && react-native link appcenter-analytics && react-native link appcenter-crashes'
401 | );
402 | }
403 | }
404 |
405 | _runInstallCommandsWithErrorMessages(
406 | commands,
407 | library,
408 | manualInstallationCommand
409 | ) {
410 | const results = commands.map(command =>
411 | this.spawnCommandSync(
412 | command.commandName,
413 | command.args || [],
414 | command.options || {}
415 | )
416 | );
417 | for (let i = 0; i < results.length; i += 1) {
418 | if (results[i].error) {
419 | this.log(
420 | `Configuration went well but there was an error while installing ${library}. Please install it manually with \`${manualInstallationCommand}\`. Then rerun the generator.`
421 | .red.bold
422 | );
423 | throw results[i].error;
424 | }
425 | break;
426 | }
427 | }
428 |
429 | _createKeystore() {
430 | const path = `android/app/${this.answers.lowerCaseProjectName}.${
431 | this.answers.environmentName
432 | }.keystore`;
433 | if (!this.fs.exists(this.destinationPath(path))) {
434 | this.spawnCommandSync('keytool', [
435 | '-genkey',
436 | '-v',
437 | '-dname',
438 | `OU=${this.answers.companyName}`,
439 | '-keystore',
440 | path,
441 | '-alias',
442 | this.answers.lowerCaseProjectName,
443 | '-keyalg',
444 | 'RSA',
445 | '-keysize',
446 | '2048',
447 | '-validity',
448 | '10000',
449 | '-storepass',
450 | this.answers.keystorePassword,
451 | '-keypass',
452 | this.answers.keystorePassword,
453 | ]);
454 | } else {
455 | this.log('Keystore already exists, skipping...'.yellow);
456 | }
457 | }
458 | }
459 |
460 | module.exports = FastlaneEnvGenerator;
461 |
--------------------------------------------------------------------------------
/generators/fastlane-env/templates/environment/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ENV: '<%= environmentName %>',
3 | };
4 |
--------------------------------------------------------------------------------
/generators/fastlane-env/templates/fastlane/env:
--------------------------------------------------------------------------------
1 | ### GLOBAL ###
2 | ENV='<%= environmentName %>'
3 | DEPLOYMENT_PLATFORM='<%= deploymentPlatform %>'
4 | REPO_GIT_BRANCH='<%= repoGitBranch %>'
5 |
6 | ### IOS ###
7 | IOS_PROJECT_PATH='ios'
8 | IOS_APP_NAME='<%= appName %>'
9 | <% if (deploymentPlatform === 'appcenter' || useCodePush) { %>IOS_APPCENTER_APP_ID='<%= iosAppCenterId %>'<% } %>
10 | IOS_PROJECT_NAME='<%= projectName %>'
11 | IOS_APP_ID='<%= appId %>'
12 | IOS_TEAM_ID='<%= appleTeamId %>'
13 | IOS_USER_ID='<%= appleId %>'
14 | IOS_PLIST_PATH='<%= projectName %>/Info.plist'
15 | <% if (deploymentPlatform === 'appstore') { %>IOS_ITC_TEAM_NAME='<%= itunesTeamName %>'<% } %>
16 | <% if (deploymentPlatform === 'appstore') { %>IOS_APPSTORECONNECT_USER_ID='<%= appstoreConnectAppleId %>'<% } %>
17 |
18 | ### IOS MATCH ###
19 | MATCH_GIT_URL='<%= matchGit %>'
20 | MATCH_GIT_BRANCH='<%= matchBranch %>'
21 | MATCH_TYPE='<%= certificateType %>'
22 | <% if (certificateType === 'enterprise') { %>MATCH_FORCE_ENTERPRISE='1'<% } %>
23 |
24 | ### IOS GYM ###
25 | GYM_SCHEME='<%= projectName %>'
26 | GYM_OUTPUT_DIRECTORY='dist'
27 | GYM_OUTPUT_NAME='app'
28 |
29 | ### IOS HOCKEY APP AND APP CENTER ###
30 | IOS_IPA_PATH='./dist/app.ipa'
31 | <% if (deploymentPlatform === 'appcenter' || useCodePush) { %>APPCENTER_USERNAME='<%= appCenterUsername %>'<% } %>
32 |
33 | ### ANDROID PROJECT ###
34 | ANDROID_PROJECT_DIR='android'
35 | ANDROID_APK_PATH='android/app/build/outputs/apk/release/app-release.apk'
36 | GRADLE_APP_IDENTIFIER='<%= appId %>'
37 | GRADLE_APP_NAME='<%= appName %>'
38 | <% if (deploymentPlatform === 'appcenter' || useCodePush) { %>ANDROID_APPCENTER_APP_ID='<%= androidAppCenterId %>'<% } %>
39 | GRADLE_KEYSTORE='<%= lowerCaseProjectName %>.<%= environmentName %>.keystore'
40 | GRADLE_KEYSTORE_ALIAS='<%= lowerCaseProjectName %>'
41 | <% if (deploymentPlatform === 'appstore') { %>ANDROID_PLAYSTORE_JSON_KEY_PATH='<%= androidPlayStoreJsonKeyPath %>'<% } %>
42 |
43 | ### CODEPUSH ###
44 | <% if (useCodePush) { %>IOS_CODEPUSH_DEPLOYMENT_NAME='<%= iosCodePushDeploymentName %>'<% } %>
45 | <% if (useCodePush) { %>ANDROID_CODEPUSH_DEPLOYMENT_NAME='<%= androidCodePushDeploymentName %>'<% } %>
46 |
--------------------------------------------------------------------------------
/generators/fastlane-env/templates/fastlane/env.secret:
--------------------------------------------------------------------------------
1 | GRADLE_KEYSTORE_PASSWORD='<%= keystorePassword %>'
2 | GRADLE_KEYSTORE_ALIAS_PASSWORD='<%= keystorePassword %>'
3 |
4 | <% if (deploymentPlatform === 'hockeyapp') { %>FL_HOCKEY_API_TOKEN='<%= hockeyAppToken %>'<% } %>
5 | <% if (deploymentPlatform === 'appcenter' || useCodePush) { %>FL_APPCENTER_API_TOKEN='<%= appCenterToken %>'<% } %>
6 |
7 | <% if (useAppcenterSDK) { %>IOS_APPCENTER_APP_SECRET='<%= iosAppcenterAppSecret %>'<% } %>
8 | <% if (useAppcenterSDK) { %>ANDROID_APPCENTER_APP_SECRET='<%= androidAppcenterAppSecret %>'<% } %>
9 |
10 | <% if (useCodePush) { %>IOS_CODEPUSH_DEPLOYMENT_KEY='<%= iosCodePushDeploymentKey %>'<% } %>
11 | <% if (useCodePush) { %>ANDROID_CODEPUSH_DEPLOYMENT_KEY='<%= androidCodePushDeploymentKey %>'<% } %>
12 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/README.md:
--------------------------------------------------------------------------------
1 | # Fastlane Setup
2 |
3 | *Fastlane is an amazing tool which allows you to easily build, sign and deploy both your iOS and Android applications.*
4 |
5 | > ***:warning: XCode 8 is required.***
6 | > Run at least once:
7 | > ```
8 | > xcode-select --install
9 | > ```
10 |
11 | **Features**
12 | - (Almost) No manual setup
13 | - Multiple Ids + Name
14 | - Centralized Fastlane environments config for both apps
15 | - JS environment
16 |
17 | This is just the fastlane setup step required to centralize your app config. Run it with
18 | ```
19 | yo rn-toolbox:fastlane-setup
20 | ```
21 |
22 | Once done head over to [fastlane-env](../fastlane-env/README.md) to setup multiple environments for your project.
23 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | require('colors');
3 | const analytics = require('../../analytics');
4 |
5 | class FastlaneGenerator extends Base {
6 | initializing() {
7 | analytics.pageview('/fastlane-setup').send();
8 | this.composeWith('rn-toolbox:checkversion');
9 | }
10 |
11 | prompting() {
12 | const config = this.fs.readJSON(this.destinationPath('package.json'));
13 | return this.prompt([
14 | {
15 | type: 'input',
16 | name: 'projectName',
17 | message: 'Please confirm the project name',
18 | default: config.name,
19 | },
20 | {
21 | type: 'confirm',
22 | name: 'commitKeystore',
23 | message: 'Commit keystore files?',
24 | default: true,
25 | },
26 | {
27 | type: 'confirm',
28 | name: 'useSecretsArchive',
29 | message:
30 | 'Would you like to use an encrypted archive to store secret files and keys?',
31 | default: true,
32 | },
33 | {
34 | type: 'confirm',
35 | name: 'useDeploymentScript',
36 | message: 'Would you like to use a deployment script?',
37 | default: true,
38 | },
39 | ]).then(answers => {
40 | this.answers = answers;
41 | this.answers.lowerCaseProjectName = answers.projectName.toLowerCase();
42 | if (answers.useSecretsArchive && answers.useDeploymentScript) {
43 | this.config.set('circleci-ready', true);
44 | this.config.save();
45 | }
46 | });
47 | }
48 |
49 | writing() {
50 | const config = this.fs.readJSON(this.destinationPath('package.json'));
51 | this.fs.copyTpl(
52 | this.templatePath('fastlane/*'),
53 | this.destinationPath('fastlane')
54 | );
55 | this.fs.copyTpl(
56 | this.templatePath('env'),
57 | this.destinationPath('fastlane/.env')
58 | );
59 | this.fs.copyTpl(
60 | this.templatePath('environment/*'),
61 | this.destinationPath('src/environment')
62 | );
63 | this.fs.copyTpl(
64 | this.templatePath('Gemfile'),
65 | this.destinationPath('Gemfile')
66 | );
67 | this.fs.copyTpl(
68 | this.templatePath('strings.xml'),
69 | this.destinationPath('android/app/src/main/res/values/strings.xml')
70 | );
71 |
72 | if (this.answers.useSecretsArchive) {
73 | this.fs.copyTpl(
74 | this.templatePath('secrets-scripts/pack-secrets.sh'),
75 | this.destinationPath(`secrets-scripts/pack-secrets.sh`),
76 | this.answers
77 | );
78 | this.fs.copyTpl(
79 | this.templatePath('secrets-scripts/unpack-secrets.sh'),
80 | this.destinationPath(`secrets-scripts/unpack-secrets.sh`),
81 | this.answers
82 | );
83 | config.scripts['pack-secrets'] = './secrets-scripts/pack-secrets.sh';
84 | config.scripts['unpack-secrets'] = './secrets-scripts/unpack-secrets.sh';
85 | }
86 |
87 | if (this.answers.useDeploymentScript) {
88 | this.fs.copyTpl(
89 | this.templatePath('deploy.sh'),
90 | this.destinationPath('deploy.sh')
91 | );
92 | config.scripts.deploy = './deploy.sh';
93 | }
94 |
95 | this.fs.writeJSON(this.destinationPath('package.json'), config, 'utf-8');
96 |
97 | this._extendGitignore();
98 | this._extendGradle();
99 | this._activateManualSigning();
100 | }
101 |
102 | install() {
103 | const bundling = this.spawnCommand('bundle', ['install'], {
104 | cwd: this.destinationPath(),
105 | });
106 |
107 | bundling.on('error', () => {
108 | this.log.error(
109 | 'Unable to run bundle install step. Please make sure you have bundler installed (gem install bundler)'
110 | .bgRed.white.bold
111 | );
112 | });
113 | }
114 |
115 | _extendGitignore() {
116 | let content = this.fs.read(this.destinationPath('.gitignore'));
117 | if (this.answers.commitKeystore) {
118 | content = content.replace('*.keystore', '');
119 | }
120 |
121 | this.fs.copyTpl(
122 | this.templatePath('gitignore'),
123 | this.destinationPath('.gitignore'),
124 | { content }
125 | );
126 | }
127 |
128 | _extendGradle() {
129 | let config = this.fs.read(this.destinationPath('android/app/build.gradle'));
130 | // Add the release signingConfig
131 | config = config.replace(
132 | /(buildTypes {\n\s*release {(?:\n.*)+?)(\n\s*})/m,
133 | '$1\n signingConfig signingConfigs.release$2'
134 | );
135 | // Add the signingConfig
136 | const gradleSigningTemplate = this.fs.read(
137 | this.templatePath('gradleSigning')
138 | );
139 | config = config.replace(
140 | /(})(\n\s*buildTypes)/m,
141 | `$1\n${gradleSigningTemplate}$2`
142 | );
143 | // Add the appName var
144 | config = config.replace(
145 | /(buildTypes {)(\s*release {\n)/m,
146 | '$1\n debug {\n resValue "string", "app_name", appName\n }$2 resValue "string", "app_name", appName\n'
147 | );
148 | // Change the app id
149 | config = config.replace(
150 | /applicationId ".*"/,
151 | 'applicationId _applicationId'
152 | );
153 | // Replace the versionCode
154 | config = config.replace(/versionCode .*/, 'versionCode _versionCode');
155 | // Replace the versionName
156 | config = config.replace(/versionName .*/, 'versionName _versionName');
157 | // Add the default params
158 | config = config.replace(
159 | /\nandroid {\n/m,
160 | `\ndef _applicationId = System.getenv("GRADLE_APP_IDENTIFIER") ?: 'com.${
161 | this.answers.lowerCaseProjectName
162 | }.debug'\ndef appName = System.getenv("GRADLE_APP_NAME") ?: '${
163 | this.answers.projectName
164 | } debug'\ndef _versionCode = (System.getenv("ANDROID_VERSION_CODE") ?: "1") as Integer\ndef _versionName = System.getenv("ANDROID_VERSION_NAME") ?: "1.0.0"\n\nandroid {\n`
165 | );
166 | // Output the file
167 | this.fs.write(this.destinationPath('android/app/build.gradle'), config);
168 | }
169 |
170 | _activateManualSigning() {
171 | let config = this.fs.read(
172 | this.destinationPath(
173 | `ios/${this.answers.projectName}.xcodeproj/project.pbxproj`
174 | )
175 | );
176 |
177 | // Manual signing
178 | config = config.replace(
179 | /ProvisioningStyle = Automatic;/g,
180 | 'ProvisioningStyle = Manual;'
181 | );
182 | config = config.replace(
183 | /(TargetAttributes = {(?:\n.*?)+?TestTargetID = )(.+?)(;\n.*\n)(.+)/m,
184 | '$1$2$3 $2 = {\n ProvisioningStyle = Manual;\n };\n$4'
185 | );
186 |
187 | // Distribution code signing Release
188 | config = config.replace(
189 | /(Release.*(?:\n.+){3}ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;)/m,
190 | '$1\n CODE_SIGN_IDENTITY = "iPhone Distribution";\n "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";\n DEVELOPMENT_TEAM = "";'
191 | );
192 |
193 | // Developer code signing Debug
194 | config = config.replace(
195 | /(Debug.*(?:\n.+){3}ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;)/m,
196 | '$1\n CODE_SIGN_IDENTITY = "iPhone Developer";\n "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";\n DEVELOPMENT_TEAM = "";'
197 | );
198 |
199 | this.fs.write(
200 | this.destinationPath(
201 | `ios/${this.answers.projectName}.xcodeproj/project.pbxproj`
202 | ),
203 | config
204 | );
205 | }
206 |
207 | end() {
208 | this.config.set('fastlane', true);
209 | this.config.save();
210 | }
211 | }
212 |
213 | module.exports = FastlaneGenerator;
214 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 | # Uncomment next line for cocoapods
5 | # gem "cocoapods"
6 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/deploy.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | set -e
3 |
4 | DEV=0
5 | RED='\033[0;31m'
6 | BLUE='\033[0;34m'
7 | CYAN='\033[0;36m'
8 | GREEN='\033[0;32m'
9 | YELLOW='\033[0;33m'
10 | NO_COLOR='\033[0m'
11 |
12 | APP_ENV="staging"
13 | APP_OS="ios and android"
14 | DEPLOY_TYPE="soft"
15 |
16 |
17 | success(){
18 | echo -e "✅ ${GREEN}$1${NO_COLOR}"
19 | }
20 |
21 | warn(){
22 | echo -e "⚠️ ${YELLOW}$1${NO_COLOR}"
23 |
24 | if [ $DEV -eq 0 ]
25 | then
26 | exit 1
27 | fi
28 | }
29 |
30 | check_environment(){
31 | CURRENT_BRANCH=`git rev-parse --abbrev-ref HEAD`
32 |
33 | if [ "$CURRENT_BRANCH" != "$REPO_GIT_BRANCH" ]
34 | then
35 | warn "Wrong branch, checkout $REPO_GIT_BRANCH to deploy to $APP_ENV."
36 | else
37 | success "Deploying to $APP_ENV."
38 | fi
39 | }
40 |
41 |
42 | while getopts ":e:o:t:d:m:" opt; do
43 | case $opt in
44 | e) APP_ENV="$OPTARG"
45 | ;;
46 | o) APP_OS="$OPTARG"
47 | ;;
48 | t) DEPLOY_TYPE="$OPTARG"
49 | ;;
50 | d) DEV=1
51 | ;;
52 | \?) echo "${RED}Invalid option -$OPTARG${NO_COLOR}" >&2
53 | ;;
54 | esac
55 | done
56 |
57 | [[ -z $(git status -s) ]] || warn 'Please make sure you deploy with no changes or untracked files. You can run *git stash --include-untracked*.'
58 |
59 | source fastlane/.env.$APP_ENV
60 |
61 | check_environment $APP_ENV
62 |
63 | if [ $DEPLOY_TYPE == "hard" ]; then
64 | echo -e "${BLUE}* * * * *"
65 | echo -e "👷 Hard-Deploy"
66 | echo -e "* * * * *${NO_COLOR}"
67 | bundle exec fastlane set_build_numbers_to_current_timestamp
68 |
69 | if [[ $APP_OS != "android" ]]; then
70 | echo -e "${GREEN}- - - - -"
71 | echo -e "Fastlane 🍎 iOS $APP_ENV"
72 | echo -e "- - - - -${NO_COLOR}"
73 | bundle exec fastlane ios deploy --env=$APP_ENV
74 | fi
75 | if [[ $APP_OS != "ios" ]]; then
76 | echo -e "${YELLOW}- - - - -"
77 | echo "Fastlane 🤖 Android $APP_ENV"
78 | echo -e "- - - - -${NO_COLOR}"
79 | bundle exec fastlane android deploy --env=$APP_ENV
80 | fi
81 | fi
82 |
83 | if [ $DEPLOY_TYPE == "soft" ]; then
84 | echo -e "${CYAN}* * * * *"
85 | echo -e "🍦 Soft-Deploy"
86 | echo -e "* * * * *${NO_COLOR}"
87 |
88 | if [[ $APP_OS != "android" ]]; then
89 | echo -e "${GREEN}- - - - -"
90 | echo -e "Codepush 🍎 iOS ${APP_ENV}"
91 | echo -e "- - - - -${NO_COLOR}"
92 | bundle exec fastlane ios deploy codepush: --env=$APP_ENV
93 | fi
94 | if [[ $APP_OS != "ios" ]]; then
95 | echo -e "${YELLOW}- - - - -"
96 | echo -e "Codepush 🤖 Android ${APP_ENV}"
97 | echo -e "- - - - -${NO_COLOR}"
98 | bundle exec fastlane android deploy codepush: --env=$APP_ENV
99 | fi
100 | fi
101 |
102 | success "📦 Deploy succeeded."
103 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/env:
--------------------------------------------------------------------------------
1 | # Build number is composed of :
2 | # 1+ digit for major version number
3 | # 2 digits for minor version number
4 | # 2 digits for patch version number
5 | # 2 digits for the release candidate
6 | # Example : Version 1.2.39 with build N°5 = Build 1023905
7 |
8 | # It is recommended to have
9 | # IOS_VERSION = ANDROID_VERSION_NAME
10 | # and
11 | # IOS_VERSION_BUILD_NUMBER = ANDROID_VERSION_CODE
12 |
13 | IOS_VERSION='1.0.0'
14 | ANDROID_VERSION_NAME='1.0.0'
15 | IOS_VERSION_BUILD_NUMBER='1000000'
16 | ANDROID_VERSION_CODE='1000000'
17 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/environment/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ENV: 'local',
3 | };
4 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | # See https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md
2 | app_identifier ENV['IOS_APP_ID']
3 | apple_id ENV['IOS_USER_ID']
4 | team_id ENV['IOS_TEAM_ID']
5 | itc_team_name ENV['IOS_ITC_TEAM_NAME']
6 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | fastlane_require 'dotenv'
2 | fastlane_require 'spaceship'
3 | release_notes_command = "git log HEAD --pretty=format:\"%s\" -1"
4 |
5 | # Check Git Status
6 |
7 | lane :check_git_status do |options|
8 | ensure_git_branch(
9 | branch: "#{options[:name]}"
10 | )
11 | ensure_git_status_clean
12 | git_pull
13 |
14 | end
15 |
16 | lane :set_build_numbers_to_current_timestamp do |options|
17 | incremented_build_number = Time.now.to_i.to_s
18 | `sed -i -e "s#.*IOS_VERSION_BUILD_NUMBER=.*#IOS_VERSION_BUILD_NUMBER='#{incremented_build_number}'#g" .env`
19 | `sed -i -e "s#.*ANDROID_VERSION_CODE=.*#ANDROID_VERSION_CODE='#{incremented_build_number}'#g" .env`
20 | ENV['IOS_VERSION_BUILD_NUMBER'] = incremented_build_number
21 | ENV['ANDROID_VERSION_CODE'] = incremented_build_number
22 | end
23 |
24 | # JS Environments
25 |
26 | lane :set_js_env do |options|
27 | backup_file(path: 'src/environment/index.js')
28 | sh "cp ../src/environment/index.#{ENV['ENV']}.js ../src/environment/index.js"
29 | end
30 |
31 | before_all do |lane, options|
32 | Dotenv.load(".env.#{ENV['ENV']}.secret")
33 | end
34 |
35 | after_all do |lane, options|
36 | if File.exist?('../src/environment/index.js.back')
37 | restore_file(path: 'src/environment/index.js')
38 | end
39 | end
40 |
41 | error do |lane, exception, options|
42 | if File.exist?('../src/environment/index.js.back')
43 | restore_file(path: 'src/environment/index.js')
44 | end
45 | end
46 |
47 | # IOS
48 |
49 | platform :ios do
50 | lane :restore_files do |options|
51 | plist_path = "#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PLIST_PATH']}"
52 | if File.exist?("../#{plist_path}.back")
53 | restore_file(path: plist_path)
54 | end
55 | pbxproj_path="#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PROJECT_NAME']}.xcodeproj/project.pbxproj"
56 | if File.exist?("../#{pbxproj_path}.back")
57 | restore_file(path: pbxproj_path)
58 | end
59 | appcenter_plist_path="#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PROJECT_NAME']}/AppCenter-Config.plist"
60 | if File.exist?("../#{appcenter_plist_path}.back")
61 | restore_file(path: appcenter_plist_path)
62 | end
63 | end
64 |
65 | error do |lane, exception, options|
66 | restore_files
67 | end
68 |
69 | lane :setup do |options|
70 | produce(
71 | app_name: ENV['IOS_APP_NAME'],
72 | skip_itc: true
73 | )
74 | match(
75 | type: 'development',
76 | shallow_clone: true,
77 | clone_branch_directly: true,
78 | force_for_new_devices: true
79 | )
80 | match(
81 | shallow_clone: true,
82 | clone_branch_directly: true,
83 | force_for_new_devices: true
84 | )
85 | end
86 |
87 | lane :setup_push do
88 | Spaceship.login
89 | Spaceship.select_team
90 | keyName = "Push JWT #{ENV['IOS_APP_NAME']} #{ENV['ENV']}".gsub!(/[^0-9A-Za-z ]/, '')
91 | key = Spaceship::Portal::Key.create(name: keyName, apns: true)
92 | puts "Key ID is"
93 | puts key.id
94 | File.write("#{keyName}.p8", key.download)
95 | end
96 |
97 | lane :build do |options|
98 | plist_full_path = "#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PLIST_PATH']}"
99 | backup_file(path: plist_full_path)
100 | xcodeproj = "#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PROJECT_NAME']}.xcodeproj"
101 | xcworkspace = "#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PROJECT_NAME']}.xcworkspace"
102 | pbxproj_full_path="#{xcodeproj}/project.pbxproj"
103 | backup_file(path: pbxproj_full_path)
104 | update_info_plist(
105 | xcodeproj: xcodeproj,
106 | plist_path: ENV['IOS_PLIST_PATH'],
107 | block: lambda { |plist|
108 | plist['CFBundleName'] = ENV['IOS_APP_NAME']
109 | plist['CFBundleDisplayName'] = ENV['IOS_APP_NAME']
110 | plist['CFBundleShortVersionString'] = ENV['IOS_VERSION']
111 | plist['CFBundleVersion'] = ENV['IOS_VERSION_BUILD_NUMBER']
112 | },
113 | )
114 | if ENV['IOS_CODEPUSH_DEPLOYMENT_KEY'] then
115 | set_info_plist_value(path: plist_full_path, key:'CodePushDeploymentKey', value: ENV['IOS_CODEPUSH_DEPLOYMENT_KEY'])
116 | end
117 | if ENV['IOS_APPCENTER_APP_SECRET'] then
118 | appcenter_plist_full_path = "#{ENV['IOS_PROJECT_PATH']}/#{ENV['IOS_PROJECT_NAME']}/AppCenter-Config.plist"
119 | backup_file(path: appcenter_plist_full_path)
120 | set_info_plist_value(path: appcenter_plist_full_path, key:'AppSecret', value: ENV['IOS_APPCENTER_APP_SECRET'])
121 | end
122 | update_app_identifier(
123 | xcodeproj: xcodeproj,
124 | plist_path: ENV['IOS_PLIST_PATH'],
125 | app_identifier: ENV['IOS_APP_ID']
126 | )
127 | profile_env_name = "sigh_#{ENV['IOS_APP_ID']}_#{ENV['MATCH_TYPE']}_profile-name"
128 | gymOptions = ({
129 | silent: true,
130 | xcargs: "PROVISIONING_PROFILE_SPECIFIER='#{ENV[profile_env_name]}' DEVELOPMENT_TEAM='#{ENV['IOS_TEAM_ID']}'"
131 | }).merge(
132 | File.directory?("../#{xcworkspace}") ?
133 | {workspace: xcworkspace} :
134 | {project: xcodeproj}
135 | )
136 | gym(gymOptions)
137 | restore_files
138 | end
139 |
140 | lane :deploy_hockey do |options|
141 | hockey(
142 | notify: '0',
143 | ipa: ENV['IOS_IPA_PATH']
144 | )
145 | end
146 |
147 | lane :deploy_to_appcenter do |options|
148 | appcenter_upload(
149 | api_token: ENV['FL_APPCENTER_API_TOKEN'],
150 | owner_name: ENV['APPCENTER_USERNAME'],
151 | app_name: ENV['IOS_APPCENTER_APP_ID'],
152 | ipa: ENV['IOS_IPA_PATH'],
153 | release_notes: %x[#{release_notes_command}]
154 | )
155 | end
156 |
157 | lane :deploy_to_testflight do |options|
158 | pilot(
159 | username: ENV['IOS_APPSTORECONNECT_USER_ID'],
160 | app_identifier: ENV['IOS_APP_ID'],
161 | ipa: ENV['IOS_IPA_PATH'],
162 | distribute_external: false,
163 | skip_waiting_for_build_processing: true
164 | )
165 | end
166 |
167 | lane :deploy do |options|
168 | set_js_env
169 | if options[:codepush] then
170 | release_notes = %x[#{release_notes_command}]
171 | sh "cd .. && appcenter codepush release-react -d #{ENV['IOS_CODEPUSH_DEPLOYMENT_NAME']} -a #{ENV['APPCENTER_USERNAME']}/#{ENV['IOS_APPCENTER_APP_ID']} --target-binary-version \"#{ENV['IOS_VERSION']}\" --description \"#{release_notes}\" --disable-duplicate-release-error"
172 | else
173 | setup_circle_ci
174 | match(
175 | shallow_clone: true,
176 | clone_branch_directly: true,
177 | readonly: true
178 | )
179 | build
180 | if ENV['DEPLOYMENT_PLATFORM'] === 'hockeyapp' then
181 | deploy_hockey
182 | elsif ENV['DEPLOYMENT_PLATFORM'] === 'appcenter' then
183 | deploy_to_appcenter
184 | elsif ENV['DEPLOYMENT_PLATFORM'] === 'appstore'
185 | deploy_to_testflight
186 | end
187 | end
188 | end
189 |
190 | lane :deploy_local do |options|
191 | check_git_status(name: ENV['REPO_GIT_BRANCH'])
192 | deploy
193 | end
194 |
195 | lane :add_devices do |options|
196 | if ['development','adhoc'].include?(ENV['MATCH_TYPE']) then
197 | register_devices(
198 | team_id: ENV['IOS_TEAM_ID'],
199 | devices_file: "./fastlane/ios_devices.txt"
200 | )
201 | match(
202 | shallow_clone: true,
203 | clone_branch_directly: true,
204 | force_for_new_devices: true
205 | )
206 | else
207 | puts 'Environment not associated to an ad-hoc or development app'
208 | end
209 | end
210 |
211 | lane :get_certificates_and_profiles do |options|
212 | match(
213 | type: 'development',
214 | shallow_clone: true,
215 | clone_branch_directly: true,
216 | readonly: true
217 | )
218 | match(
219 | shallow_clone: true,
220 | clone_branch_directly: true,
221 | readonly: true
222 | )
223 | end
224 |
225 | end
226 |
227 | # ANDROID
228 |
229 | platform :android do
230 | lane :restore_files do |options|
231 | appcenter_config_path = "#{ENV['ANDROID_PROJECT_DIR']}/app/src/main/assets/appcenter-config.json"
232 | if File.exist?("../#{appcenter_config_path}.back")
233 | restore_file(path: appcenter_config_path)
234 | end
235 | android_strings_path = "#{ENV['ANDROID_PROJECT_DIR']}/app/src/main/res/values/strings.xml"
236 | if File.exist?("../#{android_strings_path}.back")
237 | restore_file(path: android_strings_path)
238 | end
239 | end
240 |
241 | error do |lane, exception, options|
242 | restore_files
243 | end
244 |
245 | lane :set_keys do |options|
246 | if ENV['ANDROID_APPCENTER_APP_SECRET'] then
247 | appcenter_config_path = "#{ENV['ANDROID_PROJECT_DIR']}/app/src/main/assets/appcenter-config.json"
248 | backup_file(path: appcenter_config_path)
249 | appcenter_config = load_json(json_path: appcenter_config_path)
250 | appcenter_config['app_secret'] = ENV['ANDROID_APPCENTER_APP_SECRET']
251 | File.open("../#{appcenter_config_path}","w") do |f|
252 | f.write(appcenter_config.to_json)
253 | end
254 | end
255 | if ENV['ANDROID_CODEPUSH_DEPLOYMENT_KEY'] then
256 | android_strings_path = "#{ENV['ANDROID_PROJECT_DIR']}/app/src/main/res/values/strings.xml"
257 | # backup_file(path: android_strings_path) We can't backup this way because android can't build if there is a non-xml file in values folder. Solution to be found.
258 | xml = Nokogiri::XML(File.open("../#{android_strings_path}"))
259 | xml.at("//string[@name=\"reactNativeCodePush_androidDeploymentKey\"]").content = ENV['ANDROID_CODEPUSH_DEPLOYMENT_KEY']
260 | File.write("../#{android_strings_path}", xml.to_xml)
261 | end
262 | end
263 |
264 | lane :build do |options|
265 | set_keys
266 | gradle(
267 | task: "assembleRelease",
268 | project_dir: ENV['ANDROID_PROJECT_DIR']
269 | )
270 | restore_files
271 | end
272 |
273 | lane :deploy_hockey do |options|
274 | hockey(
275 | notify: '0',
276 | apk: ENV['ANDROID_APK_PATH']
277 | )
278 | end
279 |
280 | lane :deploy_to_appcenter do |options|
281 | appcenter_upload(
282 | api_token: ENV['FL_APPCENTER_API_TOKEN'],
283 | owner_name: ENV['APPCENTER_USERNAME'],
284 | app_name: ENV['ANDROID_APPCENTER_APP_ID'],
285 | apk: ENV['ANDROID_APK_PATH'],
286 | release_notes: %x[#{release_notes_command}]
287 | )
288 | end
289 |
290 | lane :deploy_to_playstore do |options|
291 | supply(
292 | package_name: ENV['GRADLE_APP_IDENTIFIER'],
293 | track: 'internal',
294 | apk: ENV['ANDROID_APK_PATH'],
295 | json_key: ENV['ANDROID_PLAYSTORE_JSON_KEY_PATH'],
296 | )
297 | end
298 |
299 | lane :deploy do |options|
300 | set_js_env
301 | if options[:codepush] then
302 | release_notes = %x[#{release_notes_command}]
303 | sh "cd .. && appcenter codepush release-react -d #{ENV['ANDROID_CODEPUSH_DEPLOYMENT_NAME']} -a #{ENV['APPCENTER_USERNAME']}/#{ENV['ANDROID_APPCENTER_APP_ID']} --target-binary-version \"#{ENV['ANDROID_VERSION_NAME']}\" --description \"#{release_notes}\" --disable-duplicate-release-error"
304 | else
305 | build
306 | if ENV['DEPLOYMENT_PLATFORM'] === 'hockeyapp' then
307 | deploy_hockey
308 | elsif ENV['DEPLOYMENT_PLATFORM'] === 'appcenter' then
309 | deploy_to_appcenter
310 | elsif ENV['DEPLOYMENT_PLATFORM'] === 'appstore' then
311 | deploy_to_playstore
312 | end
313 | end
314 | end
315 |
316 | lane :deploy_local do |options|
317 | check_git_status(name: ENV['REPO_GIT_BRANCH'])
318 | deploy
319 | end
320 |
321 | end
322 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/fastlane/ios_devices.txt:
--------------------------------------------------------------------------------
1 | Device ID Device Name
2 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Tom Example1 - iPhone X
3 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Sarah Example2 - iPhone 6 Plus
4 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Rob Example3 - iPhone 7
5 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Bob Example4 - iPhone 6s
6 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/gitignore:
--------------------------------------------------------------------------------
1 | <%= content %>
2 | # Fastlane
3 | dist
4 | fastlane/report.xml
5 | fastlane/.env.*.secret
6 | *.back
7 | .bundle
8 | vendor/bundle
9 | *._api-*.json
10 | *api-*.json
11 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/gradleSigning:
--------------------------------------------------------------------------------
1 | signingConfigs {
2 | release {
3 | storeFile file(String.valueOf(System.getenv("GRADLE_KEYSTORE")))
4 | storePassword System.getenv("GRADLE_KEYSTORE_PASSWORD")
5 | keyAlias System.getenv("GRADLE_KEYSTORE_ALIAS")
6 | keyPassword System.getenv("GRADLE_KEYSTORE_ALIAS_PASSWORD")
7 | }
8 | }
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/secrets-scripts/pack-secrets.sh:
--------------------------------------------------------------------------------
1 | ########################################################################################
2 | # Zip secrets archive, from the root of the project. Saves the location of the secrets #
3 | # for later unzipping. #
4 | # Command: ./secrets-scripts/pack-secrets.sh -e #
5 | ########################################################################################
6 |
7 | #! /bin/bash
8 | set -e
9 |
10 | ## Use to create the secrets archive by grabbing all the secret files from your current codebase.
11 | ## Useful to add/remove secrets from the archive, after having unpacked currently comited archive with unpack_secrets.sh.
12 |
13 | APP_ENV=""
14 | GREEN='\033[0;32m'
15 |
16 |
17 | while getopts ":e:" opt; do
18 | case $opt in
19 | e) APP_ENV="$OPTARG"
20 | ;;
21 | \?) echo "${RED}Invalid option -$OPTARG${NO_COLOR}" >&2
22 | ;;
23 | esac
24 | done
25 |
26 | source fastlane/.env.${APP_ENV}
27 |
28 |
29 | FILE_ROOT="${APP_ENV}_app_secrets_with_paths"
30 |
31 | # Select files to put in the archive
32 | SECRETS_TO_PACK="fastlane/.env.${APP_ENV}.secret android/app/${GRADLE_KEYSTORE}"
33 |
34 | # Create archive
35 | tar -cvzf $FILE_ROOT.tar.gz $SECRETS_TO_PACK
36 |
37 | # Encrypt archive
38 | gpg --symmetric $FILE_ROOT.tar.gz
39 |
40 | ## Remove intermediaries
41 | rm $FILE_ROOT.tar.gz
42 |
43 | echo -e "✅ ${GREEN} ${APP_ENV} secrets have been packed into ${FILE_ROOT}.tar.gz.gpg. Please commit this encrypted archive."
44 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/secrets-scripts/unpack-secrets.sh:
--------------------------------------------------------------------------------
1 | ########################################################################################
2 | # Unzip secrets archive, copying the secret files to the correct locations in the repo #
3 | # Command: ./secrets-scripts/unpack-secrets.sh -e -p #
4 | ########################################################################################
5 |
6 | #! /bin/bash
7 |
8 | set -e
9 |
10 | APP_ENV="test"
11 | SECRETS_PASSPHRASE=""
12 | GREEN='\033[0;32m'
13 |
14 |
15 | while getopts ":e:p:" opt; do
16 | case $opt in
17 | e) APP_ENV="$OPTARG"
18 | ;;
19 | p) SECRETS_PASSPHRASE="$OPTARG"
20 | ;;
21 | \?) echo "${RED}Invalid option -$OPTARG${NO_COLOR}" >&2
22 | ;;
23 | esac
24 | done
25 |
26 | # Select encrypted file
27 | FILE_ROOT="${APP_ENV}_app_secrets_with_paths"
28 |
29 | ## Decrypt
30 | gpg --decrypt --passphrase $SECRETS_PASSPHRASE --batch $FILE_ROOT.tar.gz.gpg > $FILE_ROOT.tar.gz
31 |
32 | ## Unzip to correct locations in project
33 | tar -xzvf $FILE_ROOT.tar.gz
34 |
35 | ## Remove intermediaries
36 | rm $FILE_ROOT.tar.gz
37 |
38 | echo -e "✅ ${GREEN} ${APP_ENV} secrets have been unpacked to the correct location in your local environment"
39 |
--------------------------------------------------------------------------------
/generators/fastlane-setup/templates/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/generators/jest/README.md:
--------------------------------------------------------------------------------
1 | # Jest setup
2 |
3 | *Unit tests are an essential part of developping a robust application. Jest is the ideal unit test tool for React Native.*
4 |
5 | Jest now comes bundled with React Native but this generator will setup a few extra tools.
6 |
7 | Use `yo rn-toolbox:jest` to setup jest.
8 |
9 | **Features**
10 | - Base test
11 | - File Stub to prevent errors when files are imported in components
12 | - Base test for the `Button` component if `rn-toolbox:base` was installed
13 | - Inclusion of `npm run test:lint` when running `npm test` if `rn-toolbox:lint` was installed
14 |
--------------------------------------------------------------------------------
/generators/jest/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const analytics = require('../../analytics');
3 |
4 | class JestGenerator extends Base {
5 | initializing() {
6 | analytics.pageview('/jest').send();
7 | this.composeWith('rn-toolbox:checkversion');
8 | }
9 |
10 | writing() {
11 | this.fs.copyTpl(
12 | this.templatePath('FileStub.js'),
13 | this.destinationPath('jest/FileStub.js')
14 | );
15 | if (this.config.get('base')) {
16 | this.fs.copyTpl(
17 | this.templatePath('Button.test.js'),
18 | this.destinationPath('src/components/Button.test.js')
19 | );
20 | } else {
21 | this.fs.copyTpl(
22 | this.templatePath('firstTest.js'),
23 | this.destinationPath('__tests__/firstTest.js')
24 | );
25 | }
26 | this.fs.extendJSON(
27 | 'package.json',
28 | {
29 | scripts: {
30 | 'test:unit': 'jest',
31 | test: `${
32 | this.config.get('lint') ? 'npm run test:lint && ' : ''
33 | }npm run test:unit`,
34 | },
35 | jest: {
36 | moduleNameMapper: {
37 | '^[./a-zA-Z0-9$_-]+\\.(jpg|png|gif|eot|svg|ttf|woff|woff2|mp4|webm)$':
38 | '/jest/FileStub.js',
39 | },
40 | },
41 | },
42 | null,
43 | 2
44 | );
45 | }
46 |
47 | end() {
48 | this.config.set('jest', true);
49 | this.config.save();
50 | }
51 | }
52 |
53 | module.exports = JestGenerator;
54 |
--------------------------------------------------------------------------------
/generators/jest/templates/Button.test.js:
--------------------------------------------------------------------------------
1 | import 'react-native';
2 | import React from 'react';
3 | import renderer from 'react-test-renderer';
4 |
5 | import Button from './Button';
6 |
7 | describe('Button', () => {
8 | it('should render the button', () => {
9 | const tree = renderer.create(
10 | ,
11 | ).toJSON();
12 | expect(tree).toMatchSnapshot();
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/generators/jest/templates/FileStub.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/generators/jest/templates/firstTest.js:
--------------------------------------------------------------------------------
1 | describe('FirstTest', () => {
2 | it('should be valid', () => {
3 | expect(true).toBe(true);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/generators/lint/README.md:
--------------------------------------------------------------------------------
1 | # Adding linting
2 |
3 | *Eslint ensures a consistent coding style. For React Native newcomers it will also help you in your path to learn it.*
4 |
5 | *Prettier autoformats your code as you save. No more wasting time indenting, adding a missing comma or splitting lines*
6 |
7 | Use `yo rn-toolbox:lint` to setup eslint-config-universe with prettier enabled.
8 |
--------------------------------------------------------------------------------
/generators/lint/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const analytics = require('../../analytics');
3 |
4 | class LintGenerator extends Base {
5 | initializing() {
6 | analytics.pageview('/lint').send();
7 | this.composeWith('rn-toolbox:checkversion');
8 | }
9 |
10 | install() {
11 | this.yarnInstall(['prettier', 'eslint', 'eslint-config-universe-error'], {
12 | dev: true,
13 | cwd: this.destinationRoot(),
14 | }).then(() => {
15 | this.spawnCommand('yarn', ['lint', '--', '--fix'], {
16 | cwd: this.destinationPath(),
17 | });
18 | });
19 | }
20 |
21 | writing() {
22 | this.fs.copyTpl(
23 | this.templatePath('eslintrc'),
24 | this.destinationPath('.eslintrc')
25 | );
26 | this.fs.copyTpl(
27 | this.templatePath('eslintignore'),
28 | this.destinationPath('.eslintignore')
29 | );
30 | this.fs.copyTpl(
31 | this.templatePath('prettierrc'),
32 | this.destinationPath('.prettierrc')
33 | );
34 | this.fs.extendJSON(
35 | 'package.json',
36 | {
37 | scripts: { lint: 'eslint . --quiet' },
38 | },
39 | null,
40 | 2
41 | );
42 | }
43 |
44 | end() {
45 | this.config.set('lint', true);
46 | this.config.save();
47 | }
48 | }
49 |
50 | module.exports = LintGenerator;
51 |
--------------------------------------------------------------------------------
/generators/lint/templates/eslintignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/android
3 | **/ios
4 | **/vendors
5 |
--------------------------------------------------------------------------------
/generators/lint/templates/eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["universe-error/native"],
3 | "rules": {
4 | "prettier/prettier": "error"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/generators/lint/templates/prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "singleQuote": true,
4 | "bracketSpacing": true,
5 | "tabWidth": 2,
6 | "printWidth": 100
7 | }
8 |
--------------------------------------------------------------------------------
/generators/travisci/README.md:
--------------------------------------------------------------------------------
1 | # TravisCI setup for continuous integration
2 |
3 | *TravisCI is a continuous integration solution allowing you to run your tests each time you do a pull request. We advocate using it for unit tests as it is faster than Bitrise to execute them.*
4 |
5 | In order to make Travis working on your app, you need to create a .travis.yml file by running
6 |
7 | We recommend using Travis for the tests as it comes with yarn preinstalled and allows you to save your Jest cache
8 |
9 | `yo rn-toolbox:travisci`
10 |
11 | Then you have to configure the branch on which you want to trigger travisCI webhook on :)
12 |
--------------------------------------------------------------------------------
/generators/travisci/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const analytics = require('../../analytics');
3 |
4 | class CircleGenerator extends Base {
5 | initializing() {
6 | analytics.pageview('/travisci').send();
7 | this.composeWith('rn-toolbox:checkversion');
8 | }
9 |
10 | writing() {
11 | this.fs.copyTpl(
12 | this.templatePath('travis.yml'),
13 | this.destinationPath('.travis.yml')
14 | );
15 | }
16 |
17 | end() {
18 | this.config.set('travis', true);
19 | this.config.save();
20 | }
21 | }
22 |
23 | module.exports = CircleGenerator;
24 |
--------------------------------------------------------------------------------
/generators/travisci/templates/travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | cache:
3 | directories:
4 | - jest/tmp
5 | - node_modules
6 |
--------------------------------------------------------------------------------
/generators/vscode/README.md:
--------------------------------------------------------------------------------
1 | # Visual Studio Code Setup
2 |
3 | *Sets up VSCode for ultimate productivity with React Native*
4 |
5 | Use `yo rn-toolbox:vscode` to set it up
6 |
--------------------------------------------------------------------------------
/generators/vscode/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('yeoman-generator');
2 | const templateSettings = require('./templates/settings.json');
3 | const analytics = require('../../analytics');
4 |
5 | class VSCodeGenerator extends Base {
6 | initializing() {
7 | analytics.pageview('/vscode').send();
8 | this.composeWith('rn-toolbox:checkversion');
9 | }
10 |
11 | prompting() {
12 | const config = this.fs.readJSON(this.destinationPath('package.json'));
13 | return this.prompt([
14 | {
15 | type: 'input',
16 | name: 'appName',
17 | message: 'Your react native app directory name',
18 | default: config.name,
19 | },
20 | ]).then(answers => {
21 | this.answers = answers;
22 | });
23 | }
24 | writing() {
25 | this.fs.copyTpl(
26 | this.templatePath('jsconfig.json'),
27 | this.destinationPath('jsconfig.json'),
28 | this.answers
29 | );
30 | this.fs.extendJSON(
31 | this.destinationPath('.vscode/settings.json'),
32 | templateSettings
33 | );
34 | }
35 |
36 | end() {
37 | this.config.set('vscode', true);
38 | this.config.save();
39 | }
40 | }
41 |
42 | module.exports = VSCodeGenerator;
43 |
--------------------------------------------------------------------------------
/generators/vscode/templates/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "allowSyntheticDefaultImports": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "<%= appName %>/*": "./*"
8 | }
9 | },
10 | "exclude": [
11 | "node_modules"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/generators/vscode/templates/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "javascript.validate.enable": false
3 | }
4 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6"
4 | },
5 | "exclude": [
6 | "node_modules",
7 | "**/node_modules/*"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generator-rn-toolbox",
3 | "version": "0.0.0-managed-by-semantic-release",
4 | "description": "React-Native generators to kickstart your project",
5 | "scripts": {
6 | "lint": "eslint .",
7 | "test": "echo \"Error: no test specified\" && exit 0",
8 | "commitmsg": "validate-commit-msg",
9 | "semantic-release": "semantic-release",
10 | "commit": "git-cz",
11 | "travis-deploy-once": "travis-deploy-once"
12 | },
13 | "keywords": [
14 | "yeoman-generator",
15 | "react-native",
16 | "generator",
17 | "react",
18 | "native",
19 | "toolbox",
20 | "fastlane",
21 | "eslint",
22 | "boilerplate",
23 | "environments",
24 | "circleci",
25 | "bitrise",
26 | "travisci",
27 | "circleci",
28 | "ci",
29 | "wallabyjs",
30 | "visual studio code",
31 | "vscode"
32 | ],
33 | "repository": {
34 | "type": "git",
35 | "url": "https://github.com/bamlab/generator-rn-toolbox"
36 | },
37 | "dependencies": {
38 | "bluebird": "^3.5.0",
39 | "color-js": "^1.0.4",
40 | "colors": "^1.1.2",
41 | "compare-version": "^0.1.2",
42 | "download-file-sync": "^1.0.4",
43 | "fs-extra": "^4.0.2",
44 | "gm": "^1.23.0",
45 | "randomstring": "^1.1.5",
46 | "universal-analytics": "^0.4.20",
47 | "yeoman-generator": "^2.0.1"
48 | },
49 | "devDependencies": {
50 | "commitizen": "^2.9.6",
51 | "cross-spawn": "^5.1.0",
52 | "cz-conventional-changelog": "^2.0.0",
53 | "eslint": "^4.10.0",
54 | "eslint-config-universe": "^1.0.7",
55 | "husky": "^0.14.3",
56 | "jest": "^21.2.1",
57 | "prettier": "^1.11.1",
58 | "semantic-release": "^15.9.9",
59 | "travis-deploy-once": "^5.0.2",
60 | "validate-commit-msg": "^2.14.0",
61 | "yeoman-environment": "^2.0.5",
62 | "yeoman-test": "^1.7.0"
63 | },
64 | "config": {
65 | "commitizen": {
66 | "path": "./node_modules/cz-conventional-changelog"
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | require('colors');
3 | const path = require('path');
4 | const spawn = require('cross-spawn');
5 | const helpers = require('yeoman-test');
6 |
7 | const appName = 'TestApp';
8 |
9 | function createProject() {
10 | console.log('## Creating project ##'.cyan);
11 | spawn.sync('react-native', ['init', appName], {
12 | cwd: __dirname,
13 | stdio: 'inherit',
14 | });
15 | }
16 |
17 | function installLint() {
18 | console.log('## Installing Lint ##'.cyan);
19 | return helpers
20 | .run(path.join(__dirname, 'generators/lint'))
21 | .cd(path.join(__dirname, appName))
22 | .withOptions({ skipInstall: false })
23 | .toPromise();
24 | }
25 |
26 | function installBase() {
27 | console.log('## Installing Base Project ##'.cyan);
28 | return helpers
29 | .run(path.join(__dirname, 'generators/base'))
30 | .cd(path.join(__dirname, appName))
31 | .withOptions({ skipInstall: false })
32 | .withPrompts({ appName })
33 | .toPromise();
34 | }
35 |
36 | function installJest() {
37 | console.log('## Installing Jest ##'.cyan);
38 | return helpers
39 | .run(path.join(__dirname, 'generators/jest'))
40 | .cd(path.join(__dirname, appName))
41 | .withOptions({ skipInstall: false })
42 | .withPrompts({ appName })
43 | .toPromise();
44 | }
45 |
46 | function installFastlane() {
47 | console.log('## Installing Fastlane ##'.cyan);
48 | return helpers
49 | .run(path.join(__dirname, 'generators/fastlane'))
50 | .cd(path.join(__dirname, appName))
51 | .withOptions({ skipInstall: false })
52 | .withPrompts({
53 | companyName: 'BAM',
54 | appName,
55 | projectName: appName,
56 | stagingAppId: 'tech.bam.rntest.staging',
57 | prodAppId: 'tech.bam.rntest',
58 | matchGit: process.env.RN_MATCH_GIT,
59 | appleId: process.env.RN_APPLE_ID,
60 | stagingAppleTeamId: process.env.RN_STAGING_APPLE_TEAM_ID,
61 | prodAppleTeamId: process.env.RN_PROD_APPLE_TEAM_ID,
62 | itunesTeamId: process.env.RN_ITUNES_TEAM_ID,
63 | keystorePassword: 'TestTest',
64 | hockeyAppToken: process.env.RN_HOCKEY_APP_TOKEN,
65 | })
66 | .toPromise();
67 | }
68 |
69 | function installBitrise() {
70 | console.log('## Installing Bitrise ##'.cyan);
71 | return helpers
72 | .run(path.join(__dirname, 'generators/bitrise'))
73 | .cd(path.join(__dirname, appName))
74 | .withOptions({ skipInstall: false })
75 | .withPrompts({
76 | reactNativeDirectory: '.',
77 | androidProdAppId: 'tech.bam.rntest',
78 | })
79 | .toPromise();
80 | }
81 |
82 | function testProject() {
83 | console.log('## Testing Project ##'.cyan);
84 | spawn.sync('npm', ['test'], {
85 | cwd: path.join(__dirname, appName),
86 | stdio: 'inherit',
87 | });
88 | }
89 |
90 | createProject();
91 | installLint()
92 | .then(installBase)
93 | .then(installJest)
94 | .then(installFastlane)
95 | .then(installBitrise)
96 | .then(testProject);
97 |
--------------------------------------------------------------------------------