;
32 | }
33 |
34 | const executeAsyncRequest = async ({
35 | values,
36 | request,
37 | onPrefetch,
38 | onSuccess,
39 | successSelector = response => response.data,
40 | onError,
41 | failureSelector = response => ({ problem: response.problem, errorData: response.data }),
42 | onPostFetch
43 | }: AsyncRequest
) => {
44 | onPrefetch();
45 | const response = await request(values);
46 | if (response.ok) {
47 | onSuccess(successSelector(response));
48 | } else {
49 | onError(failureSelector(response));
50 | }
51 | onPostFetch(response);
52 | };
53 |
54 | // Returns a request to execute manually at some point, and the variables that will be updated when it does
55 | export const useAsyncRequest =
({
56 | initialState = null,
57 | request,
58 | withSuccessSelector,
59 | withPostSuccess,
60 | withFailureSelector,
61 | withPostFailure,
62 | withPostFetch
63 | }: AsyncRequestHookParams
): [Nullable, boolean, Nullable>, (params: P) => void] => {
64 | const [state, setState] = useState(initialState);
65 | const [loading, setLoading] = useState(false);
66 | const [error, setError] = useState>>(null);
67 | const sendRequest = useCallback(
68 | values => {
69 | executeAsyncRequest({
70 | values,
71 | request,
72 | onPrefetch: () => setLoading(true),
73 | onSuccess: data => {
74 | setState(data!);
75 | setError(null);
76 | if (withPostSuccess) withPostSuccess(data);
77 | },
78 | successSelector: withSuccessSelector,
79 | onError: errorInfo => {
80 | setError(errorInfo);
81 | if (withPostFailure) withPostFailure(errorInfo);
82 | },
83 | failureSelector: withFailureSelector,
84 | onPostFetch: response => {
85 | setLoading(false);
86 | if (withPostFetch) withPostFetch(response);
87 | }
88 | });
89 | },
90 | [request, withFailureSelector, withPostFailure, withPostFetch, withPostSuccess, withSuccessSelector]
91 | );
92 |
93 | return [state, loading, error, sendRequest];
94 | };
95 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/i18n.ejs:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 | import Routes from '@constants/routes';
3 |
4 | // TODO: Replace here the screens titles
5 |
6 | i18next.addResources('es', 'app', {
7 | <%_ if(features.loginandsignup) { _%>
8 | [Routes.Login]: 'Login',
9 | [Routes.SignUp]: 'SignUp',
10 | <%_ } _%>
11 | <%_ if(features.tabs) { _%>
12 | [Routes.Tab1]: 'Tab 1',
13 | [Routes.Tab2]: 'Tab 2',
14 | <%_ } _%>
15 | [Routes.Home]: 'Home'<%_ if(features.onboarding) { _%>,
16 | [Routes.OnBoarding]: 'OnBoarding'
17 | <%_ } _%>
18 | });
19 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import Reactotron from 'reactotron-react-native';
4 | import SplashScreen from 'react-native-splash-screen';
5 | import AppNavigator from '@components/AppNavigator';
6 | import { ErrorHandler } from '@components/ErrorBoundary';
7 | import { ExceptionHandler } from '@components/ErrorBoundary/ExceptionHandler';
8 | import { apiSetup } from '@config/api';
9 | import { actionCreators as AuthActions } from '@redux/auth/actions';
10 | import './i18n';
11 |
12 | const App = () => {
13 | const dispatch = useDispatch();
14 |
15 | useEffect(() => {
16 | SplashScreen.hide();
17 | }, []);
18 |
19 | useEffect(() => {
20 | apiSetup();
21 | dispatch(AuthActions.init());
22 | ExceptionHandler();
23 | }, [dispatch]);
24 |
25 | return (
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | const MyAppWithOverlay = __DEV__ ? Reactotron.overlay(App) : App;
33 |
34 | export default MyAppWithOverlay;
35 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/constants.ts:
--------------------------------------------------------------------------------
1 | export const FIELDS = {
2 | name: 'name',
3 | surname: 'surname',
4 | birthDate: 'birthDate',
5 | sex: 'sex',
6 | email: 'email',
7 | password: 'password',
8 | phoneNumber: 'phoneNumber'
9 | } as const;
10 |
11 | export interface SignupFormValues {
12 | [FIELDS.name]: string;
13 | [FIELDS.surname]: string;
14 | [FIELDS.birthDate]: string;
15 | [FIELDS.sex]: string;
16 | [FIELDS.email]: string;
17 | [FIELDS.password]: string;
18 | [FIELDS.phoneNumber]: string;
19 | }
20 |
21 | export interface LoginFormValues {
22 | [FIELDS.email]: string;
23 | [FIELDS.password]: string;
24 | }
25 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/screens/Login/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 |
3 | i18next.addResources('es', 'LOGIN', {
4 | MAIL: 'Email',
5 | MAIL_PLACEHOLDER: 'Ej: email@dominio.com',
6 | PASSWORD: 'Contraseña',
7 | LOG_IN: 'Iniciar sesión',
8 | LOGIN_FAILURE: 'Email y/o contraseña incorrecto/s',
9 | SIGN_UP: 'No tenes cuenta? Registrate!'
10 | });
11 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/screens/Login/index.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as redux from 'react-redux';
3 | import { render, fireEvent, waitFor, waitForElementToBeRemoved } from '@testing-library/react-native';
4 | import Login from '@authScreens/Login';
5 |
6 | const INVALID_EMAIL = 'hello';
7 | const VALID_EMAIL = 'email@email.com';
8 | const VALID_PASSWORD = 'HelloWord1234';
9 |
10 | describe('', () => {
11 | test('Invalid inputs', async () => {
12 | const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
13 | const dispatch = jest.fn();
14 | useDispatchSpy.mockReturnValue(dispatch);
15 |
16 | const { getByText, getAllByText, getByTestId } = render();
17 |
18 | const submitButton = getByText('Iniciar sesión');
19 | const emailInput = getByTestId('Email');
20 |
21 | fireEvent.press(submitButton);
22 |
23 | await waitFor(() => expect(getAllByText('Este campo es obligatorio')).toHaveLength(2));
24 |
25 | fireEvent.changeText(emailInput, INVALID_EMAIL);
26 | fireEvent.press(submitButton);
27 |
28 | await waitFor(() => getByText('El formato del mail es inválido'));
29 |
30 | fireEvent.changeText(emailInput, VALID_EMAIL);
31 | fireEvent.press(submitButton);
32 |
33 | await waitForElementToBeRemoved(() => getByText('El formato del mail es inválido'));
34 | expect(dispatch).toHaveBeenCalledTimes(0);
35 | });
36 |
37 | test('Log in', async () => {
38 | const navigation = {
39 | navigate: jest.fn()
40 | };
41 | const useDispatchSpy = jest.spyOn(redux, 'useDispatch');
42 | const dispatch = jest.fn();
43 | useDispatchSpy.mockReturnValue(dispatch);
44 | const { getByText, getByTestId } = render();
45 | const emailInput = getByTestId('Email');
46 | const passwordInput = getByTestId('Contraseña');
47 | const submitButton = getByText('Iniciar sesión');
48 | fireEvent.changeText(emailInput, VALID_EMAIL);
49 | fireEvent.changeText(passwordInput, VALID_PASSWORD);
50 | await waitFor(() => fireEvent.press(submitButton));
51 | expect(dispatch).toHaveBeenCalled();
52 | useDispatchSpy.mockClear();
53 | });
54 |
55 | test('Sign up', () => {
56 | const navigation = {
57 | navigate: jest.fn()
58 | };
59 | const spy = jest.spyOn(navigation, 'navigate');
60 | const { getByText } = render();
61 | const button = getByText('No tenes cuenta? Registrate!');
62 | fireEvent.press(button);
63 | expect(spy).toHaveBeenCalled();
64 | spy.mockClear();
65 | });
66 |
67 | test('Login Snapshot', () => {
68 | const spy = jest.spyOn(redux, 'useSelector');
69 | spy.mockReturnValue({ auth: { currentUserError: null } });
70 | const login = render().toJSON();
71 | expect(login).toMatchSnapshot();
72 | spy.mockClear();
73 | });
74 |
75 | test('Login Error Snapshot', () => {
76 | const spy = jest.spyOn(redux, 'useSelector');
77 | spy.mockReturnValue({ auth: { currentUserError: 'error' } });
78 | const login = render();
79 | const { getByText } = login;
80 | getByText('Email y/o contraseña incorrecto/s');
81 | expect(login.toJSON()).toMatchSnapshot();
82 | spy.mockClear();
83 | });
84 | });
85 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/screens/Login/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Keyboard, TouchableWithoutFeedback, View } from 'react-native';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import i18next from 'i18next';
5 | import { useForm } from 'react-hook-form';
6 | import CustomButton from '@components/CustomButton';
7 | import CustomText from '@components/CustomText';
8 | import ControlledCustomTextInput from '@components/CustomTextInput/controller';
9 | import Routes from '@constants/routes';
10 | import { Navigation } from '@interfaces/navigation';
11 | import { State } from '@interfaces/reduxInterfaces';
12 | import { actionCreators as AuthActions } from '@redux/auth/actions';
13 | import { FIELDS, LoginFormValues } from '@screens/Auth/constants';
14 | import { validateRequired, validateEmail } from '@utils/validations/validateUtils';
15 |
16 | import './i18n';
17 | import styles from './styles';
18 |
19 | function Login({ navigation }: Navigation) {
20 | const dispatch = useDispatch();
21 | const hasLoginError = useSelector((state: State) => !!state.auth.currentUserError);
22 |
23 | const { handleSubmit, control } = useForm({ mode: 'onBlur' });
24 |
25 | const handleLogin = (values: LoginFormValues) => dispatch(AuthActions.login(values));
26 |
27 | const handleGoToSignUp = () => navigation.navigate(Routes.SignUp);
28 | return (
29 |
30 |
31 |
32 |
42 |
52 | {hasLoginError && (
53 |
54 | {i18next.t('LOGIN:LOGIN_FAILURE')}
55 |
56 | )}
57 |
58 |
63 |
68 |
69 |
70 | );
71 | }
72 |
73 | export default Login;
74 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/screens/Login/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { green } from '@constants/colors';
3 |
4 | export default StyleSheet.create({
5 | container: {
6 | alignItems: 'center',
7 | flex: 1,
8 | justifyContent: 'center',
9 | width: '100%'
10 | },
11 | form: {
12 | width: 250
13 | },
14 | formButton: {
15 | backgroundColor: green,
16 | padding: 10,
17 | borderRadius: 3,
18 | marginTop: 15
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/screens/SignUp/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 |
3 | i18next.addResources('es', 'SIGNUP', {
4 | NAME: 'Nombre/s',
5 | SURNAME: 'Apellido/s',
6 | BIRTH_DATE: 'Fecha de nacimiento',
7 | BIRTH_DATE_PLACEHOLDER: 'Ej: DD/MM/AAAA',
8 | MAIL: 'Email',
9 | MAIL_PLACEHOLDER: 'Ej: email@dominio.com',
10 | SEX: 'Sexo',
11 | SEX_PLACEHOLDER: 'Ej: Masculino/Femenino',
12 | PASSWORD: 'Contraseña',
13 | PHONE_NUMBER: 'Número de teléfono',
14 | PHONE_NUMBER_PLACEHOLDER: 'Número sin 0 ni 15. Ej: 1134454325',
15 | SIGN_UP: 'Registrarse',
16 | SIGNUP_FAILURE: 'Ocurrió un error. Por favor inténtenlo nuevamente!'
17 | });
18 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Auth/screens/SignUp/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { green } from '@constants/colors';
3 |
4 | export default StyleSheet.create({
5 | container: {
6 | alignItems: 'center',
7 | justifyContent: 'center',
8 | width: '100%'
9 | },
10 | stretchAndFlex: {
11 | alignSelf: 'stretch'
12 | },
13 | form: {
14 | paddingBottom: 20,
15 | paddingHorizontal: 60
16 | },
17 | formButton: {
18 | backgroundColor: green,
19 | borderRadius: 3,
20 | marginTop: 15,
21 | padding: 10
22 | }
23 | });
24 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Home/index.ejs:
--------------------------------------------------------------------------------
1 | import React, { <%_ if(features.loginandsignup) { _%>useCallback, <%_ } _%>memo } from 'react';
2 | import { View } from 'react-native';
3 | <%_ if(features.loginandsignup) { _%>
4 | import { useDispatch } from 'react-redux';
5 | import CustomButton from '@components/CustomButton';
6 | import { actionCreators as AuthActions } from '@redux/auth/actions';
7 | <%_ } else { _%>
8 | import CustomText from '@components/CustomText';
9 | <%_ } _%>
10 |
11 | import styles from './styles';
12 |
13 | function Home() {
14 | <%_ if(features.loginandsignup) { _%>
15 | const dispatch = useDispatch();
16 | const handleLogout = useCallback(() => dispatch(AuthActions.logout()), [dispatch]);
17 | <%_ } _%>
18 | return (
19 |
20 | <%_ if(features.loginandsignup) { _%>
21 |
22 | <%_ } else { _%>
23 | <%= projectName %>
24 | <%_ } _%>
25 |
26 | );
27 | }
28 |
29 | export default memo(Home);
30 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/Home/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { green } from '@constants/colors';
3 |
4 | export default StyleSheet.create({
5 | container: {
6 | flex: 1,
7 | alignItems: 'center',
8 | justifyContent: 'center'
9 | },
10 | home: {
11 | backgroundColor: green,
12 | padding: 10,
13 | borderRadius: 3
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/FirstScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import i18next from 'i18next';
4 | import CustomText from '@components/CustomText';
5 |
6 | import styles from './styles';
7 |
8 | function FirstScreen() {
9 | return (
10 |
11 | {i18next.t('ONBOARDING:FIRST_SCREEN')}
12 |
13 | );
14 | }
15 |
16 | export default FirstScreen;
17 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/FirstScreen/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | alignItems: 'center'
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/Footer/buttonsInfo.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 |
3 | import { FooterProps } from './interface';
4 |
5 | export const getScreensButtonsInfo = ({ onNextScreen, onSkip, onPreviousScreen, screenIndex }: FooterProps) =>
6 | [
7 | {
8 | firstButton: {
9 | onPress: onSkip,
10 | title: i18next.t('ONBOARDING:SKIP')
11 | },
12 | secondButton: {
13 | onPress: onNextScreen,
14 | title: i18next.t('ONBOARDING:NEXT')
15 | }
16 | },
17 | {
18 | firstButton: {
19 | onPress: onPreviousScreen,
20 | title: i18next.t('ONBOARDING:PREVIOUS')
21 | },
22 | secondButton: {
23 | onPress: onNextScreen,
24 | title: i18next.t('ONBOARDING:NEXT')
25 | }
26 | },
27 | {
28 | firstButton: {
29 | onPress: onPreviousScreen,
30 | title: i18next.t('ONBOARDING:PREVIOUS')
31 | },
32 | secondButton: {
33 | onPress: onSkip,
34 | title: i18next.t('ONBOARDING:FINISH')
35 | }
36 | }
37 | ][screenIndex];
38 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import CustomButton from '@components/CustomButton';
4 |
5 | import { FooterProps } from './interface';
6 | import { getScreensButtonsInfo } from './buttonsInfo';
7 | import styles from './styles';
8 |
9 | function Footer(props: FooterProps) {
10 | const { firstButton, secondButton } = getScreensButtonsInfo(props);
11 | return (
12 |
13 | {firstButton && }
14 | {secondButton && }
15 |
16 | );
17 | }
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/Footer/interface.ts:
--------------------------------------------------------------------------------
1 | export interface FooterProps {
2 | onNextScreen: () => void;
3 | onSkip: () => void;
4 | onPreviousScreen: () => void;
5 | screenIndex: number;
6 | }
7 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/Footer/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { blue } from '@constants/colors';
3 |
4 | export default StyleSheet.create({
5 | buttonContainer: {
6 | padding: 10,
7 | borderWidth: 1,
8 | borderRadius: 4,
9 | borderColor: blue,
10 | width: 100
11 | },
12 | buttons: {
13 | flexDirection: 'row',
14 | justifyContent: 'space-between',
15 | width: '100%',
16 | paddingHorizontal: 10
17 | }
18 | });
19 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/SecondScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import i18next from 'i18next';
4 | import CustomText from '@components/CustomText';
5 |
6 | import styles from './styles';
7 |
8 | function SecondScreen() {
9 | return (
10 |
11 | {i18next.t('ONBOARDING:SECOND_SCREEN')}
12 |
13 | );
14 | }
15 |
16 | export default SecondScreen;
17 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/SecondScreen/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | alignItems: 'center'
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/ThirdScreen/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View } from 'react-native';
3 | import i18next from 'i18next';
4 | import CustomText from '@components/CustomText';
5 |
6 | import styles from './styles';
7 |
8 | function ThirdScreen() {
9 | return (
10 |
11 | {i18next.t('ONBOARDING:THIRD_SCREEN')}
12 |
13 | );
14 | }
15 |
16 | export default ThirdScreen;
17 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/components/ThirdScreen/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 |
3 | export default StyleSheet.create({
4 | container: {
5 | flex: 1,
6 | alignItems: 'center'
7 | }
8 | });
9 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useCallback } from 'react';
2 | import { SafeAreaView } from 'react-native';
3 | import Swiper from 'react-native-swiper';
4 |
5 | import Footer from './components/Footer';
6 | import screens from './screens';
7 | import styles from './styles';
8 |
9 | interface Props {
10 | onSkip: () => void;
11 | }
12 |
13 | function CustomStepSwipper({ onSkip }: Props) {
14 | const [scrollIndex, setScrollIndex] = useState(0);
15 | const scrollView = useRef(null);
16 |
17 | const handleNextScreen = useCallback(() => {
18 | scrollView.current!.scrollBy(1);
19 | setScrollIndex(scrollIndex + 1);
20 | }, [scrollIndex]);
21 | const handlePreviousScreen = useCallback(() => {
22 | scrollView.current!.scrollBy(-1);
23 | setScrollIndex(scrollIndex - 1);
24 | }, [scrollIndex]);
25 | return (
26 |
27 |
34 | {screens}
35 |
36 |
42 |
43 | );
44 | }
45 |
46 | export default CustomStepSwipper;
47 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/screens.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import FirstScreen from './components/FirstScreen';
4 | import SecondScreen from './components/SecondScreen';
5 | import ThirdScreen from './components/ThirdScreen';
6 |
7 | export default [
8 | ,
9 | ,
10 |
11 | ];
12 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/components/Swiper/styles.ts:
--------------------------------------------------------------------------------
1 | import { StyleSheet } from 'react-native';
2 | import { blue, black, green } from '@constants/colors';
3 |
4 | const DOT_SIZE = 12;
5 | const ACTIVE_DOT_SIZE = 15;
6 | const BORDER_RADIUS = 10;
7 |
8 | const styles = StyleSheet.create({
9 | container: {
10 | flex: 1,
11 | backgroundColor: green
12 | },
13 | pagination: {
14 | position: 'absolute',
15 | bottom: 34
16 | },
17 | activeDot: {
18 | backgroundColor: blue,
19 | width: ACTIVE_DOT_SIZE,
20 | height: ACTIVE_DOT_SIZE,
21 | borderRadius: BORDER_RADIUS
22 | },
23 | dot: {
24 | backgroundColor: black,
25 | width: DOT_SIZE,
26 | height: DOT_SIZE,
27 | borderRadius: 10
28 | }
29 | });
30 |
31 | export default styles;
32 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 |
3 | i18next.addResources('es', 'ONBOARDING', {
4 | SKIP: 'Skip',
5 | NEXT: 'Next',
6 | PREVIOUS: 'Previous',
7 | FINISH: 'Finish',
8 | FIRST_SCREEN: 'First Screen',
9 | SECOND_SCREEN: 'Second Screen',
10 | THIRD_SCREEN: 'Third Screen'
11 | });
12 |
--------------------------------------------------------------------------------
/generators/app/templates/src/app/screens/OnBoarding/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { actionCreators } from '@redux/auth/actions';
4 |
5 | import './i18n';
6 | import Swiper from './components/Swiper';
7 |
8 | function OnBoardingContainer() {
9 | const dispatch = useDispatch();
10 | const handleSkipOnBoarding = () => dispatch(actionCreators.setHasAccessOnBoarding(true));
11 | return ;
12 | }
13 |
14 | export default memo(OnBoardingContainer);
15 |
--------------------------------------------------------------------------------
/generators/app/templates/src/config/api.ts:
--------------------------------------------------------------------------------
1 | import { create, NETWORK_ERROR } from 'apisauce';
2 | import Config from 'react-native-config';
3 | import Reactotron from 'reactotron-react-native';
4 | import { camelCaseSerializer, snakeCaseSerializer } from '@constants/serializers';
5 |
6 | const AUTHORIZATION_HEADER = 'Authorization';
7 | const BEARER = 'Bearer';
8 | const baseURL = Config.API_BASE_URL || 'http://wolox.com';
9 |
10 | const api = create({
11 | baseURL,
12 | timeout: 5000
13 | });
14 |
15 | export const setApiHeaders = (token: string) => {
16 | api.setHeader(AUTHORIZATION_HEADER, `${BEARER} ${token}`);
17 | };
18 |
19 | export const removeApiHeaders = () => {
20 | api.deleteHeader(AUTHORIZATION_HEADER);
21 | };
22 |
23 | export const apiSetup = () => {
24 | if (baseURL === 'http://wolox.com') {
25 | console.warn('API baseURL has not been properly initialized');
26 | }
27 | api.addResponseTransform(response => {
28 | if (response.data) response.data = camelCaseSerializer.serialize(response.data);
29 | });
30 | api.addRequestTransform(request => {
31 | if (request.data) request.data = snakeCaseSerializer.serialize(request.data);
32 | if (request.params) request.params = snakeCaseSerializer.serialize(request.params);
33 | });
34 | api.addMonitor(response => {
35 | if (response.status === 401) {
36 | // dispatch(actions.sessionExpired());
37 | console.warn('Unhandled session expiration');
38 | }
39 | });
40 | api.addMonitor(response => {
41 | if (response.problem === NETWORK_ERROR) {
42 | // dispatch(actions.noInternetConnection());
43 | console.warn('Unhandled request without connection');
44 | }
45 | });
46 | api.addMonitor(((Reactotron as unknown) as { apisauce: any }).apisauce);
47 | };
48 |
49 | export default api;
50 |
--------------------------------------------------------------------------------
/generators/app/templates/src/config/fonts.ts:
--------------------------------------------------------------------------------
1 | import { black } from '@constants/colors';
2 | import { SEMIBOLD, BOLD, ITALIC, SIZES } from '@constants/fonts';
3 | import { fontMaker } from '@utils/fontUtils';
4 |
5 | // Here you can make your custom fonts
6 | // I only recommend using family if you have more than one Font Family in the App.
7 | export default {
8 | baseFont: fontMaker({ size: SIZES.MEDIUM, color: black }),
9 | baseItalicFont: fontMaker({ size: SIZES.MEDIUM, color: black, style: ITALIC }),
10 | semiBoldFont: fontMaker({ weight: SEMIBOLD, size: SIZES.MEDIUM, color: black }),
11 | boldFont: fontMaker({ weight: BOLD, size: SIZES.MEDIUM, color: black })
12 | };
13 |
--------------------------------------------------------------------------------
/generators/app/templates/src/config/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next, { Module } from 'i18next';
2 | import { getLocales } from 'react-native-localize';
3 |
4 | const getLanguage = {
5 | type: 'languageDetector' as Module['type'],
6 | init: () => null,
7 | detect: () => {
8 | const locales = getLocales();
9 | return locales[0].languageCode;
10 | },
11 | cacheUserLanguage: () => null
12 | };
13 |
14 | i18next.use(getLanguage).init({
15 | fallbackLng: 'es',
16 | initImmediate: false
17 | });
18 |
--------------------------------------------------------------------------------
/generators/app/templates/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import './reactotronConfig';
2 | import './i18n';
3 |
--------------------------------------------------------------------------------
/generators/app/templates/src/config/navigation.ejs:
--------------------------------------------------------------------------------
1 | import { StackNavigationOptions } from '@react-navigation/stack';
2 | import i18next from 'i18next';
3 | import Routes from '@constants/routes';
4 | import { blue, white } from '@constants/colors';
5 | import statusBarConfig from '@constants/statusBar';
6 | import { Navigation } from '@interfaces/navigation';
7 |
8 | import fonts from './fonts';
9 |
10 | // Default nav options for all screens
11 | const defaultNavOptions = ({ route }: Navigation) => ({
12 | // Change screen title from i18n traslates files
13 | headerTitle: i18next.t(`app:${route.name}`),
14 | // TODO: The following options are examples. Change them to your need
15 | headerStyle: {
16 | backgroundColor: blue
17 | },
18 | headerBackTitleStyle: {
19 | color: white
20 | },
21 | headerTitleStyle: {
22 | ...fonts.baseFont,
23 | color: white
24 | },
25 | headerTintColor: white
26 | });
27 |
28 | export const appStackNavConfig = {
29 | screenOptions: defaultNavOptions
30 | };
31 | <%_ if(features.loginandsignup) { _%>
32 |
33 | export const authStackNavConfig = {
34 | screenOptions: defaultNavOptions,
35 | initialRouteName: Routes.Login
36 | };
37 | <%_ } _%>
38 | <%_ if(features.tabs) { _%>
39 |
40 | const defaultTabNavOptions = {
41 | // TODO: Change them to your need
42 | }
43 |
44 | export const tabNavConfig = {
45 | // TODO: Change them to your need
46 | // tabBarOptions: {
47 | // activeTintColor: 'blue',
48 | // inactiveTintColor: 'gray',
49 | // }
50 | screenOptions: defaultTabNavOptions
51 | };
52 | <%_ } _%>
53 | <%_ if(features.drawer) { _%>
54 |
55 | const defaultDrawerNavOptions = {
56 | // TODO: Change them to your need
57 | };
58 |
59 | export const drawerStackNavConfig = {
60 | screenOptions: defaultDrawerNavOptions
61 | };
62 | <%_ } _%>
63 |
64 | // Default nav options for all screens
65 | export const appScreensNavOptions: Partial> = {
66 | // TODO: Add here the screens nav options that changes with respect to
67 | // the default ones defined in defaultNavOptions, for example...
68 | <%_ if(features.loginandsignup) { _%>
69 | [Routes.Login]: {
70 | headerShown: false
71 | },
72 | <%_ } _%>
73 | <%_ if(features.onboarding) { _%>
74 | [Routes.OnBoarding]: {
75 | headerShown: false
76 | },
77 | <%_ } _%>
78 | [Routes.Home]: {
79 | headerTitle: 'Home'
80 | }
81 | };
82 |
83 | export const statusBarStyles = {
84 | // TODO: Change these styles to customize the status bar
85 | <%_ if (features.loginandsignup) { _%>
86 | [Routes.Login]: statusBarConfig.whiteStatusBar,
87 | [Routes.SignUp]: statusBarConfig.blueStatusBar,
88 | <%_ } _%>
89 | <%_ if (features.tabs) { _%>
90 | [Routes.Tab1]: statusBarConfig.blueStatusBar,
91 | [Routes.Tab2]: statusBarConfig.blueStatusBar,
92 | <%_ } _%>
93 | [Routes.Home]: statusBarConfig.blueStatusBar,
94 | default: statusBarConfig.transparentStatusBar
95 | };
96 |
--------------------------------------------------------------------------------
/generators/app/templates/src/config/reactotronConfig.ejs:
--------------------------------------------------------------------------------
1 | import Immutable from 'seamless-immutable';
2 | import AsyncStorage from '@react-native-async-storage/async-storage';
3 | import Reactotron, { overlay, trackGlobalErrors } from 'reactotron-react-native';
4 | import ReactotronFlipper from 'reactotron-react-native/dist/flipper';
5 | import apisaucePlugin from 'reactotron-apisauce';
6 | import { reactotronRedux } from 'reactotron-redux';
7 | import { NativeModules } from 'react-native';
8 | import { Tron } from '@interfaces/reactotron';
9 |
10 | // Console augmentation
11 | declare global {
12 | interface Console {
13 | tron: Tron;
14 | }
15 | }
16 |
17 | // If you want to use a physical device and connect it to reactotron, execute first 'adb reverse tcp:9090 tcp:9090'
18 | if (__DEV__) {
19 | const { scriptURL } = NativeModules.SourceCode;
20 | const scriptHostname = scriptURL.split('://')[1].split(':')[0];
21 | Reactotron.configure({
22 | name: '<%= projectName %>',
23 | host: scriptHostname,
24 | createSocket: path => new ReactotronFlipper(path)
25 | })
26 | .use(trackGlobalErrors({}))
27 | .use(apisaucePlugin())
28 | .use(
29 | reactotronRedux({
30 | onRestore: state =>
31 | Object.entries(state).reduce(
32 | (prev, [key, value]) => ({ ...prev, [key]: key === 'nav' ? value : Immutable(value) }),
33 | {}
34 | )
35 | })
36 | )
37 | .use(overlay())
38 | .setAsyncStorageHandler?.(AsyncStorage)
39 | .connect();
40 |
41 | // eslint-disable-next-line no-console
42 | console.tron = {
43 | log: Reactotron.logImportant,
44 | clear: Reactotron.clear,
45 | customCommand: Reactotron.onCustomCommand,
46 | display: Reactotron.display
47 | };
48 | }
49 |
50 | /* Here is an example of how to use customCommand
51 |
52 | const selfRemoving = console.tron.customCommand({
53 | command: "remove",
54 | handler: () => {
55 | selfRemoving() // Calling it unregisters the command
56 | },
57 | })
58 |
59 | This will display a button in Reactotron which will execute the whole handler
60 | block when clicked.
61 |
62 | Is important to know that a customCommand can't be declared/registered twice
63 | So we have to unregister it if we are going to run the same block again.
64 | If you use this pattern the customCommand will be unregistered when executed
65 | to avoid conflics in the future. A good way to register a customCommand is in the
66 | ComponentDidMount life cycle method of the desired component
67 |
68 | */
69 |
70 | export default Reactotron;
71 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/colors.ts:
--------------------------------------------------------------------------------
1 | export const blue: string = '#288CCB';
2 | export const green: string = '#53E69D';
3 | export const red: string = '#FF0000';
4 | export const transparent: string = 'transparent';
5 | export const white: string = '#FFF';
6 | export const black: string = '#000';
7 | export const gray: string = '#rgba(0, 0, 0, 0.38)';
8 | export const darkGray: string = '#363636';
9 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/fonts.ts:
--------------------------------------------------------------------------------
1 | // FONTS
2 | // You can add here your custom fonts.
3 | // If you want to read more about naming and linking custom fonts,
4 | // this is the original post: https://medium.com/@mehran.khan/ultimate-guide-to-use-custom-fonts-in-react-native-77fcdf859cf4
5 | export const NUNITO = 'Nunito';
6 | // WEIGHTS
7 | export const REGULAR = 'Regular';
8 | export const SEMIBOLD = 'SemiBold';
9 | export const BOLD = 'Bold';
10 | // STYLES
11 | export const NORMAL = 'Normal';
12 | export const ITALIC = 'Italic';
13 | // SIZES
14 | export const SIZES = {
15 | XXSMALL: 10,
16 | XSMALL: 12,
17 | SMALL: 14,
18 | MEDIUM: 16,
19 | XMEDIUM: 18,
20 | BIG: 20,
21 | XBIG: 36
22 | };
23 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/platform.ts:
--------------------------------------------------------------------------------
1 | import { Platform, StatusBar, Dimensions } from 'react-native';
2 |
3 | export const ROOT = 'root';
4 |
5 | export const isAndroid = Platform.OS === 'android';
6 | export const isIos = Platform.OS === 'ios';
7 |
8 | const IOS_STATUS_BAR_HEIGHT = 20;
9 | const NATIVE_BAR_CURRENT_HEIGHT = StatusBar.currentHeight || 0;
10 | export const STATUS_BAR_HEIGHT = isIos ? IOS_STATUS_BAR_HEIGHT : NATIVE_BAR_CURRENT_HEIGHT;
11 | export const STATUS_BAR_IS_FIXED = isAndroid && Platform.Version < 21;
12 | export const ACTION_BAR_HEIGHT = STATUS_BAR_IS_FIXED ? 74 : 64;
13 | export const TABBAR_HEIGHT = 50;
14 |
15 | const windowDimensions = Dimensions.get('window');
16 | export const WINDOW_HEIGHT = windowDimensions.height;
17 | export const WINDOW_WIDTH = windowDimensions.width;
18 |
19 | export const VIEWPORT_HEIGHT =
20 | WINDOW_HEIGHT - TABBAR_HEIGHT - ACTION_BAR_HEIGHT - (STATUS_BAR_IS_FIXED ? STATUS_BAR_HEIGHT : 0);
21 |
22 | export const IS_SMALL_DEVICE = WINDOW_HEIGHT < 570;
23 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/routes.ejs:
--------------------------------------------------------------------------------
1 | enum Routes {
2 | <%_ if(features.loginandsignup) { _%>
3 | Auth = 'Auth',
4 | Login = 'Login',
5 | SignUp = 'SignUp',
6 | <%_ } _%>
7 | <%_ if(features.onboarding) { _%>
8 | OnBoarding = 'OnBoarding',
9 | <%_ } _%>
10 | <%_ if(features.tabs) { _%>
11 | Tab1 = 'Tab1',
12 | Tab2 = 'Tab2',
13 | <%_ } _%>
14 | App = 'App',
15 | Home = 'Home'
16 | };
17 |
18 | export default Routes;
19 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/routesParamList.ejs:
--------------------------------------------------------------------------------
1 | import Routes from '@constants/routes';
2 |
3 | export type RoutesParamList = {
4 | <%_ if(features.loginandsignup) { _%>
5 | [Routes.Auth]: undefined;
6 | [Routes.Login]: undefined;
7 | [Routes.SignUp]: undefined;
8 | <%_ } _%>
9 | <%_ if(features.onboarding) { _%>
10 | [Routes.OnBoarding]: undefined;
11 | <%_ } _%>
12 | <%_ if(features.tabs) { _%>
13 | [Routes.Tab1]: undefined;
14 | [Routes.Tab2]: undefined;
15 | <%_ } _%>
16 | [Routes.App]: undefined;
17 | [Routes.Home]: undefined;
18 | };
19 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/serializers.ts:
--------------------------------------------------------------------------------
1 | import { CamelcaseSerializer, SnakecaseSerializer } from 'cerealizr';
2 |
3 | export const snakeCaseSerializer = new SnakecaseSerializer();
4 | export const camelCaseSerializer = new CamelcaseSerializer();
5 |
--------------------------------------------------------------------------------
/generators/app/templates/src/constants/statusBar.ts:
--------------------------------------------------------------------------------
1 | import { blue, white } from './colors';
2 |
3 | const statusBarConfig = {
4 | transparentStatusBar: {
5 | barStyle: 'dark-content',
6 | translucent: true,
7 | backgroundColor: 'rgba(255, 255, 255, 0.6)'
8 | },
9 | blueStatusBar: { barStyle: 'light-content', backgroundColor: blue },
10 | whiteStatusBar: { barStyle: 'dark-content', backgroundColor: white }
11 | } as const;
12 |
13 | export default statusBarConfig;
14 |
--------------------------------------------------------------------------------
/generators/app/templates/src/interfaces/authInterfaces.ts:
--------------------------------------------------------------------------------
1 | export interface CurrentUser {
2 | sessionToken: string;
3 | }
4 |
5 | export interface AuthData {
6 | email: string;
7 | password: string;
8 | }
9 |
10 | export interface SignUpData {
11 | name: string;
12 | surname: string;
13 | birthDate: string;
14 | sex: string;
15 | email: string;
16 | password: string;
17 | phoneNumber?: string;
18 | }
19 |
--------------------------------------------------------------------------------
/generators/app/templates/src/interfaces/globalInterfaces.ts:
--------------------------------------------------------------------------------
1 | type Key = string | number;
2 | export type GenericObjectInterface = {
3 | [key in Key]: T;
4 | };
5 |
6 | export type StringObject = GenericObjectInterface;
7 | export type NumberObject = GenericObjectInterface;
8 |
9 | export type Nullable = T | null;
10 |
--------------------------------------------------------------------------------
/generators/app/templates/src/interfaces/navigation.ts:
--------------------------------------------------------------------------------
1 | import { RouteProp, NavigationProp, NavigationState } from '@react-navigation/native';
2 |
3 | export interface Navigation {
4 | route: RouteProp, string>;
5 | navigation: NavigationProp, string, NavigationState, {}, {}>;
6 | }
7 |
--------------------------------------------------------------------------------
/generators/app/templates/src/interfaces/reactotron.ts:
--------------------------------------------------------------------------------
1 | interface CustomCommand {
2 | command: string;
3 | description?: string;
4 | title: string;
5 | handler: () => void;
6 | }
7 |
8 | interface Display {
9 | name: string;
10 | value: any;
11 | preview?: string;
12 | important?: boolean;
13 | image?: string;
14 | }
15 |
16 | export interface Tron {
17 | log?: (...args: any[]) => void;
18 | clear?: () => void;
19 | customCommand: (arg: CustomCommand) => void;
20 | display: (arg: Display) => void;
21 | }
22 |
--------------------------------------------------------------------------------
/generators/app/templates/src/interfaces/reduxInterfaces.ejs:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'react';
2 | import { ApiOkResponse, ApiErrorResponse } from 'apisauce';
3 | <%_ if(features.loginandsignup) { _%>
4 | import { CurrentUser } from '@interfaces/authInterfaces';
5 | import { Nullable } from '@interfaces/globalInterfaces';
6 | <%_ } _%>
7 |
8 | export interface Action {
9 | [key: string]: any;
10 | type: string;
11 | target?: string;
12 | payload?: T;
13 | key?: string;
14 | index?: number;
15 | service?: Function;
16 | injections?: any[];
17 | successSelector?: (response: ApiOkResponse) => K;
18 | failureSelector?: (response: ApiErrorResponse
) => K;
19 | }
20 |
21 | export type DispatcheableAction = (
22 | dispatch: Dispatch>,
23 | getState: () => State
24 | ) => void;
25 |
26 | export interface AuthState {
27 | initialLoading: boolean;
28 | <%_ if(features.loginandsignup) { _%>
29 | currentUser: Nullable;
30 | currentUserLoading: boolean;
31 | currentUserError: Nullable;
32 | <%_ } _%>
33 | <%_ if(features.onboarding) { _%>
34 | hasAccessOnBoarding: boolean;
35 | <%_ } _%>
36 | }
37 |
38 | export interface State {
39 | auth: AuthState;
40 | }
41 |
42 | export interface ReduxObject {
43 | getState: () => State;
44 | }
45 |
--------------------------------------------------------------------------------
/generators/app/templates/src/redux/auth/actions.ejs:
--------------------------------------------------------------------------------
1 | <%_ if(features.loginandsignup) { _%>
2 | import { ApiOkResponse } from 'apisauce';
3 | import { Dispatch } from 'react';
4 | import { createTypes, completeTypes, withPostSuccess } from 'redux-recompose';
5 | import { setApiHeaders, removeApiHeaders } from '@config/api';
6 | import { CurrentUser, AuthData } from '@interfaces/authInterfaces';
7 | import { Nullable } from '@interfaces/globalInterfaces';
8 | import { Action, State } from '@interfaces/reduxInterfaces';
9 | import { login, logout } from '@services/AuthService';
10 |
11 | export const actions = createTypes(
12 | completeTypes({ primaryActions: ['LOGIN', 'LOGOUT'], ignoredActions: ['AUTH_INIT'<%_ if(features.onboarding) { _%>, 'HAS_ACCESS'<%_ } _%>] }),
13 | '@@AUTH'
14 | );
15 |
16 | const TARGETS = {
17 | <%_ if(features.onboarding) { _%>
18 | ONBOARDING: 'hasAccessOnBoarding',
19 | <%_ } _%>
20 | CURRENT_USER: 'currentUser'
21 | };
22 |
23 | export const actionCreators = {
24 | init: () => (dispatch: Dispatch>>, getState: () => State) => {
25 | <%_ if(features.onboarding) { _%>
26 | const { currentUser, hasAccessOnBoarding } = getState().auth;
27 | <%_ } else { _%>
28 | const { currentUser } = getState().auth;
29 | <%_ } _%>
30 | if (currentUser) setApiHeaders(currentUser.sessionToken);
31 | dispatch({
32 | type: actions.AUTH_INIT,
33 | target: TARGETS.CURRENT_USER,
34 | <%_ if(features.onboarding) { _%>
35 | hasAccessOnBoarding,
36 | <%_ } _%>
37 | payload: currentUser
38 | });
39 | },
40 | login: (authData: AuthData) => ({
41 | type: actions.LOGIN,
42 | target: TARGETS.CURRENT_USER,
43 | service: login,
44 | payload: authData,
45 | injections: [
46 | withPostSuccess((_: any, response: ApiOkResponse) => {
47 | setApiHeaders(response.data?.sessionToken!);
48 | })
49 | ]
50 | }),
51 | logout: () => ({
52 | type: actions.LOGOUT,
53 | target: TARGETS.CURRENT_USER,
54 | service: logout,
55 | successSelector: () => null,
56 | injections: [
57 | withPostSuccess((<%_ if(features.onboarding) { _%>dispatch: Dispatch<%_ } _%>) => {
58 | removeApiHeaders();
59 | <%_ if(features.onboarding) { _%>
60 | dispatch(actionCreators.setHasAccessOnBoarding(false));
61 | <%_ } _%>
62 | })
63 | ]
64 | })<%_ if(features.onboarding) { _%>,
65 | setHasAccessOnBoarding: (value: boolean) => ({
66 | type: actions.HAS_ACCESS,
67 | target: TARGETS.ONBOARDING,
68 | payload: value
69 | })
70 | <%_ }_%>
71 | };
72 | <%_ } else if (features.onboarding) { _%>
73 | import { Dispatch } from 'react';
74 | import { createTypes } from 'redux-recompose';
75 | import { Action, State } from '@interfaces/reduxInterfaces';
76 |
77 | export const actions = createTypes(['AUTH_INIT', 'HAS_ACCESS'], '@@AUTH');
78 |
79 | const TARGETS = {
80 | ONBOARDING: 'hasAccessOnBoarding'
81 | };
82 |
83 | export const actionCreators = {
84 | init: () => (dispatch: Dispatch>, getState: () => State) => {
85 | const { hasAccessOnBoarding } = getState().auth;
86 | dispatch({
87 | type: actions.AUTH_INIT,
88 | target: TARGETS.ONBOARDING,
89 | payload: hasAccessOnBoarding
90 | });
91 | },
92 | setHasAccessOnBoarding: (value: boolean) => ({
93 | type: actions.HAS_ACCESS,
94 | target: TARGETS.ONBOARDING,
95 | payload: value
96 | })
97 | };
98 | <%_ } else { _%>
99 | import { createTypes } from 'redux-recompose';
100 |
101 | export const actions = createTypes(['AUTH_INIT'], '@@AUTH');
102 |
103 | export const actionCreators = {
104 | init: () => ({ type: actions.AUTH_INIT })
105 | };
106 | <%_ } _%>
107 |
--------------------------------------------------------------------------------
/generators/app/templates/src/redux/auth/reducer.ejs:
--------------------------------------------------------------------------------
1 | <%_ if(features.loginandsignup) { _%>
2 | import { createReducer, completeReducer, completeState<%_ if(features.onboarding) { _%>, onReadValue<%_ } _%> } from 'redux-recompose';
3 | import Immutable, { ImmutableObject } from 'seamless-immutable';
4 | import { CurrentUser } from '@interfaces/authInterfaces';
5 | import { Nullable } from '@interfaces/globalInterfaces';
6 | import { Action, AuthState } from '@interfaces/reduxInterfaces';
7 |
8 | import { actions } from './actions';
9 |
10 | const stateDescription = {
11 | description: {
12 | currentUser: null
13 | },
14 | ignoredTargets: {
15 | initialLoading: true<%_ if(features.onboarding) { _%>,
16 | hasAccessOnBoarding: false
17 | <%_ }_%>
18 | }
19 | };
20 |
21 | export const initialState = completeState(stateDescription);
22 |
23 | const reducerDescription = {
24 | primaryActions: [actions.LOGIN, actions.LOGOUT],
25 | override: {
26 | <%_ if(features.onboarding) { _%>
27 | [actions.HAS_ACCESS]: onReadValue(),
28 | <%_ } _%>
29 | [actions.AUTH_INIT]: (state: ImmutableObject, action: Action>) =>
30 | state.merge({ initialLoading: false, [action.target as string]: action.payload<%_ if(features.onboarding) { _%>, hasAccessOnBoarding: action.hasAccessOnBoarding <%_ }_%> })
31 | }
32 | };
33 |
34 | export default createReducer(Immutable(initialState), completeReducer(reducerDescription));
35 | <%_ } else if (features.onboarding) { _%>
36 | import { createReducer, onReadValue } from 'redux-recompose';
37 | import Immutable, { ImmutableObject } from 'seamless-immutable';
38 | import { Action, AuthState } from '@interfaces/reduxInterfaces';
39 |
40 | import { actions } from './actions';
41 |
42 | const initialState = {
43 | hasAccessOnBoarding: false,
44 | initialLoading: true
45 | };
46 |
47 | const reducerDescription = {
48 | [actions.AUTH_INIT]: (state: ImmutableObject, action: Action) =>
49 | state.merge({ initialLoading: false, [action.target as string]: action.payload }),
50 | [actions.HAS_ACCESS]: onReadValue()
51 | };
52 |
53 | export default createReducer(Immutable(initialState), reducerDescription);
54 | <%_ } else { _%>
55 | import { createReducer } from 'redux-recompose';
56 | import Immutable, { ImmutableObject } from 'seamless-immutable';
57 | import { AuthState } from '@interfaces/reduxInterfaces';
58 |
59 | import { actions } from './actions';
60 |
61 | const initialState = {
62 | initialLoading: true
63 | };
64 |
65 | const reducerDescription = {
66 | [actions.AUTH_INIT]: (state: ImmutableObject) => state.merge({ initialLoading: false })
67 | };
68 |
69 | export default createReducer(Immutable(initialState), reducerDescription);
70 | <%_ } _%>
71 |
--------------------------------------------------------------------------------
/generators/app/templates/src/redux/middlewares/analyticsMiddleware.ts:
--------------------------------------------------------------------------------
1 | // TODO: Uncomment lines when you start using them
2 | // TODO: Use later when you want to catch some redux actions here in this middleware
3 | // import analytics from '@react-native-firebase/analytics';
4 | import { Dispatch } from 'react';
5 | // import { ReduxObject } from '@interfaces/reduxInterfaces';
6 |
7 | const eventsTrackingMiddleware = (/* TODO: { getState }: ReduxObject in the future*/) => (
8 | next: Dispatch
9 | ) => (action: any) => {
10 | switch (action.type) {
11 | // TODO: Here catch redux actions and use analytics
12 | default:
13 | break;
14 | }
15 | return next(action);
16 | // TODO: Add other actions
17 | };
18 |
19 | export default eventsTrackingMiddleware;
20 |
--------------------------------------------------------------------------------
/generators/app/templates/src/redux/store.ejs:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
2 | import Reactotron from 'reactotron-react-native';
3 | import thunk from 'redux-thunk';
4 | import { fetchMiddleware, configureMergeState } from 'redux-recompose';
5 | import AsyncStorage from '@react-native-async-storage/async-storage';
6 | import { persistReducer } from 'redux-persist';
7 | import {
8 | seamlessImmutableReconciler,
9 | seamlessImmutableTransformCreator
10 | } from 'redux-persist-seamless-immutable';
11 | import { ImmutableObject } from 'seamless-immutable';
12 | import { State } from '@interfaces/reduxInterfaces';
13 |
14 | import auth from './auth/reducer';
15 | <%_ if(features.firebaseanalytics) { _%>
16 | import AnalyticsMiddleware from './middlewares/analyticsMiddleware';
17 | <%_ } _%>
18 |
19 | const transformerConfig = {
20 | whitelistPerReducer: {
21 | <%_ if(features.loginandsignup || features.onboarding) { _%>
22 | auth: [
23 | <%_ if(features.loginandsignup && features.onboarding) { _%>
24 | 'currentUser', 'hasAccessOnBoarding'
25 | <%_ } else { _%>
26 | <%_ if(features.loginandsignup) { _%>
27 | 'currentUser'
28 | <%_ } else { _%>
29 | 'hasAccessOnBoarding'
30 | <%_ } _%>
31 | <%_ } _%>
32 | ]
33 | <%_ } else { _%>
34 | // TODO: Complete with reducers, for example
35 | // auth: ['currentUser']
36 | <%_ } _%>
37 | }
38 | };
39 |
40 | const persistConfig = {
41 | key: 'root',
42 | storage: AsyncStorage,
43 | <%_ if(features.loginandsignup || features.onboarding) { _%>
44 | whitelist: ['auth'],
45 | <%_ } else { _%>
46 | // TODO: Complete with reducers, for example
47 | // whitelist: ['auth']
48 | whitelist: [],
49 | <%_ } _%>
50 | stateReconciler: seamlessImmutableReconciler,
51 | transforms: [seamlessImmutableTransformCreator(transformerConfig)]
52 | };
53 |
54 | configureMergeState((state: ImmutableObject, diff: State) => state.merge(diff));
55 |
56 | const reducers = combineReducers({
57 | auth
58 | });
59 |
60 | const persistedReducer = persistReducer(persistConfig, reducers);
61 |
62 | const middlewares = [];
63 | const enhancers = [];
64 |
65 | /* ------------- Thunk Middleware ------------- */
66 | middlewares.push(thunk);
67 |
68 | /* ------------- Redux-Recompose Middleware ------------- */
69 | middlewares.push(fetchMiddleware);
70 |
71 | <%_ if(features.firebaseanalytics) { _%>
72 | /* ------------- Analytics Middleware ------------- */
73 | middlewares.push(AnalyticsMiddleware);
74 |
75 | <%_ } _%>
76 | /* ------------- Assemble Middleware ------------- */
77 | enhancers.push(applyMiddleware(...middlewares));
78 |
79 | if (__DEV__ && Reactotron.createEnhancer) enhancers.push(Reactotron.createEnhancer(true));
80 |
81 | // In DEV mode, we'll create the store through Reactotron
82 | const store = createStore(persistedReducer, compose(...enhancers));
83 |
84 | if (__DEV__ && Reactotron.setReduxStore) Reactotron.setReduxStore(store);
85 |
86 | export default store;
87 |
--------------------------------------------------------------------------------
/generators/app/templates/src/services/AuthService.ts:
--------------------------------------------------------------------------------
1 | import { ApiResponse } from 'apisauce';
2 | import { AuthData, SignUpData } from '@interfaces/authInterfaces';
3 |
4 | export const login = (_: AuthData) => {
5 | // TODO: Implement call to authentication API here
6 | // TODO: If you want to test the error
7 | // return Promise.resolve({
8 | // ok: false,
9 | // problem: 'CLIENT_ERROR',
10 | // originalError: {}
11 | // });
12 | return Promise.resolve({
13 | ok: true,
14 | problem: null,
15 | originalError: null,
16 | data: { sessionToken: 'token' }
17 | });
18 | };
19 |
20 | export const logout = () => {
21 | // TODO: Implement call to authentication API here
22 | // TODO: If you want to test the error
23 | // return Promise.resolve({
24 | // ok: false,
25 | // problem: 'CLIENT_ERROR',
26 | // originalError: {}
27 | // });
28 | return Promise.resolve({
29 | ok: true,
30 | problem: null,
31 | originalError: null,
32 | data: null
33 | });
34 | };
35 |
36 | export const signup = (_: SignUpData) => {
37 | // TODO: Implement call to registration API here
38 | // TODO: If you want to test the error
39 | // return Promise.resolve({
40 | // ok: false,
41 | // problem: 'CLIENT_ERROR',
42 | // originalError: {},
43 | // data: 'Error en el signup!'
44 | // }) as Promise>;
45 | return Promise.resolve({
46 | ok: true,
47 | problem: null,
48 | originalError: null,
49 | data: {}
50 | }) as Promise>;
51 | };
52 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/arrayUtils.ts:
--------------------------------------------------------------------------------
1 | import { GenericObjectInterface } from '@interfaces/globalInterfaces';
2 |
3 | export function arrayToObject(arr: Array) {
4 | const obj: GenericObjectInterface = {};
5 | arr.forEach((elem, i) => (obj[i] = elem));
6 | return obj;
7 | }
8 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/fontUtils.ts:
--------------------------------------------------------------------------------
1 | import { TextStyle } from 'react-native';
2 | import { isAndroid } from '@constants/platform';
3 | import { NUNITO, REGULAR, SEMIBOLD, BOLD, NORMAL, ITALIC } from '@constants/fonts';
4 | import { moderateScale } from '@utils/scalingUtils';
5 | import { StringObject } from '@interfaces/globalInterfaces';
6 |
7 | const REGULAR_WEIGHT: string = '400';
8 | const NORMAL_STYLE: string = 'normal';
9 |
10 | interface FontMakerOptions {
11 | size?: number;
12 | color?: string;
13 | weight?: string;
14 | family?: string;
15 | style?: string;
16 | }
17 |
18 | interface Types {
19 | weights: StringObject;
20 | styles: StringObject;
21 | }
22 |
23 | // Here you can replace NUNITO with your custom font.
24 | // Also, you can add or remove some weights or styles that don't apply with your custom font.
25 | const fonts: Record = {
26 | [NUNITO]: {
27 | weights: {
28 | [REGULAR]: REGULAR_WEIGHT,
29 | [SEMIBOLD]: '600',
30 | [BOLD]: '700'
31 | },
32 | styles: {
33 | [NORMAL]: NORMAL_STYLE,
34 | [ITALIC]: 'italic'
35 | }
36 | }
37 | };
38 |
39 | export const fontMaker = (options: FontMakerOptions = {}): TextStyle => {
40 | const { size = null, color = null } = options;
41 | let { weight = null, style = null, family = NUNITO } = options;
42 |
43 | let font = {};
44 | const { weights, styles } = fonts[family];
45 |
46 | if (isAndroid) {
47 | weight = weight !== REGULAR && weights?.[weight!] ? weight : '';
48 | style = style !== NORMAL && styles?.[style!] ? style : '';
49 |
50 | family = family.split(' ').join('');
51 | const suffix = weight! + style!;
52 |
53 | font = { fontFamily: family + (suffix.length ? `-${suffix}` : '') };
54 | } else {
55 | weight = weights?.[weight!] || weights?.[REGULAR] || REGULAR_WEIGHT;
56 | style = styles?.[style!] || styles?.[NORMAL] || NORMAL_STYLE;
57 |
58 | font = { fontFamily: family, fontWeight: weight, fontStyle: style };
59 | }
60 |
61 | font = size ? { ...font, fontSize: moderateScale(size) } : font;
62 | font = color ? { ...font, color } : font;
63 |
64 | return font;
65 | };
66 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/navUtils.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import { CommonActions } from '@react-navigation/native';
3 | import { appScreensNavOptions } from '@config/navigation';
4 | import Routes from '@constants/routes';
5 | import { Navigation } from '@interfaces/navigation';
6 |
7 | export function inferRoute(NavigationStructure: any) {
8 | return function screenComponent(screenName: Routes, component: ReactNode) {
9 | return (
10 |
15 | );
16 | };
17 | }
18 |
19 | export const onResetStack = (
20 | navigation: Navigation['navigation'],
21 | nextRoutes: { name: string; params?: any }[],
22 | initialRoute = Routes.Home
23 | ) => {
24 | navigation.dispatch(
25 | CommonActions.reset({
26 | index: 0,
27 | routes: [{ name: initialRoute }, ...nextRoutes]
28 | })
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/scalingUtils.ts:
--------------------------------------------------------------------------------
1 | import { WINDOW_HEIGHT, WINDOW_WIDTH } from '@constants/platform';
2 |
3 | const [shortDimension, longDimension] =
4 | WINDOW_WIDTH < WINDOW_HEIGHT ? [WINDOW_WIDTH, WINDOW_HEIGHT] : [WINDOW_HEIGHT, WINDOW_WIDTH];
5 |
6 | // Guideline sizes are based on standard ~5" screen mobile device
7 | const guidelineBaseWidth = 350;
8 | const guidelineBaseHeight = 680;
9 | const scaleFactor = 0.5;
10 |
11 | const scale = (size: number) => (shortDimension / guidelineBaseWidth) * size;
12 | const verticalScale = (size: number) => (longDimension / guidelineBaseHeight) * size;
13 |
14 | const moderateScale = (size: number, factor: number = scaleFactor) => size + (scale(size) - size) * factor;
15 |
16 | export { scale, verticalScale, moderateScale };
17 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/styleUtils.ts:
--------------------------------------------------------------------------------
1 | export function getCustomStyles(variants: string[], props: any, styles: any, stylePrefix: string = '') {
2 | return variants
3 | .map(variant => (props[variant] ? styles[`${variant}${stylePrefix}`] : null))
4 | .filter(style => style !== null);
5 | }
6 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/validations/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 |
3 | i18next.addResources('es', 'VALIDATIONS', {
4 | REQUIRED_FIELD: 'Este campo es obligatorio',
5 | INVALID_EMAIL: 'El formato del mail es inválido',
6 | ALPHANUMERIC: 'Este campo solo admite caracteres alfanuméricos',
7 | ONLY_TEXT: 'Este campo solo admite texto',
8 | ONLY_NUMBERS: 'Este campo solo admite números',
9 | MIN_LENGTH: 'Este campo debe tener como mínimo {{count}} caracter',
10 | MIN_LENGTH_plural: 'Este campo debe tener como mínimo {{count}} caracteres',
11 | MAX_LENGTH: 'Este campo debe tener como máximo {{count}} caracter',
12 | MAX_LENGTH_plural: 'Este campo debe tener como máximo {{count}} caracteres',
13 | EQUAL_LENGTH: 'Este campo debe tener {{count}} caracter',
14 | EQUAL_LENGTH_plural: 'Este campo debe tener {{count}} caracteres'
15 | });
16 |
--------------------------------------------------------------------------------
/generators/app/templates/src/utils/validations/validateUtils.ts:
--------------------------------------------------------------------------------
1 | import i18next from 'i18next';
2 |
3 | import './i18n';
4 |
5 | // REGEXS
6 | const emailRegex = /^(([^<>()·=~ºªÇ¨?¿*^|#¢∞¬÷"$%"≠´}{![\]\\.,;:\s@"]+(\.[^<>·$%&/=~ºªÇ¨?¿*^|#¢∞¬÷""≠´}{!()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
7 | const alphanumericRegex = /^[a-záéíóúäëïºöüA-ZÁÉÍÓÚÄËÏÖÜ0-9 ]*$/g;
8 | const onlyTextRegex = /^[a-zA-Z\s]*$/;
9 | const onlyNumberRegex = /^[0-9]*$/g;
10 |
11 | // VALIDATIONS
12 | export const validateRequired = {
13 | required: {
14 | value: true,
15 | message: i18next.t('VALIDATIONS:REQUIRED_FIELD')
16 | }
17 | };
18 |
19 | export const validateEmail = {
20 | pattern: {
21 | value: emailRegex,
22 | message: i18next.t('VALIDATIONS:INVALID_EMAIL')
23 | }
24 | };
25 |
26 | export const validateAlphanumeric = {
27 | validate: (value: string) =>
28 | !!value.match(alphanumericRegex) || (i18next.t('VALIDATIONS:ALPHANUMERIC') as string)
29 | };
30 |
31 | export const validateOnlyText = {
32 | validate: (value: string) => !!value.match(onlyTextRegex) || (i18next.t('VALIDATIONS:ONLY_TEXT') as string)
33 | };
34 |
35 | export const validateOnlyNumber = {
36 | validate: (value: string) =>
37 | !!value.match(onlyNumberRegex) || (i18next.t('VALIDATIONS:ONLY_NUMBERS') as string)
38 | };
39 |
40 | export const validateMinLength = (minValue: number) => ({
41 | minLength: { value: minValue, message: i18next.t('VALIDATIONS:MIN_LENGTH', { count: minValue }) }
42 | });
43 |
44 | export const validateMaxLength = (maxValue: number) => ({
45 | maxLength: { value: maxValue, message: i18next.t('VALIDATIONS:MAX_LENGTH', { count: maxValue }) }
46 | });
47 |
48 | export const validateEqualLength = (equalValue: number) => ({
49 | validate: (value: string) =>
50 | value.length === equalValue || (i18next.t('VALIDATIONS:EQUAL_LENGTH', { count: equalValue }) as string)
51 | });
52 |
--------------------------------------------------------------------------------
/generators/app/templates/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "allowSyntheticDefaultImports": true,
5 | "noImplicitAny": true,
6 | "esModuleInterop": true,
7 | "isolatedModules": true,
8 | "jsx": "react-native",
9 | "lib": ["es6"],
10 | "moduleResolution": "node",
11 | "noEmit": true,
12 | "strict": true,
13 | "target": "esnext",
14 | "baseUrl": "./",
15 | "paths": {
16 | "@app/*": ["src/app/*"],
17 | "@authScreens/*": ["./src/app/screens/Auth/screens/*"],
18 | "@components/*": ["src/app/components/*"],
19 | "@config/*": ["src/config/*"],
20 | "@constants/*": ["src/constants/*"],
21 | "@hooks/*": ["src/app/hooks/*"],
22 | "@interfaces/*": ["src/interfaces/*"],
23 | "@navigationHelper": ["src/app/components/AppNavigator/helper.ts"],
24 | "@redux/*": ["src/redux/*"],
25 | "@screens/*": ["./src/app/screens/*"],
26 | "@services/*": ["src/services/*"],
27 | "@utils/*": ["src/utils/*"]
28 | },
29 | "skipLibCheck": true
30 | },
31 | "include": ["index.d.ts", "src/**/*"],
32 | "exclude": [ ".history", "node_modules", "**/node_modules/*"]
33 | }
34 |
--------------------------------------------------------------------------------
/generators/bitrise/apis/bitbucketApiConfig.js:
--------------------------------------------------------------------------------
1 | const apisauce = require('apisauce');
2 |
3 | const baseURL = 'https://api.bitbucket.org/2.0';
4 |
5 | const bitbucketApi = apisauce.create({
6 | baseURL,
7 | timeout: 30000
8 | });
9 |
10 | bitbucketApi.addMonitor(response => {
11 | if (!response.ok) {
12 | console.log(response.data);
13 | }
14 | });
15 |
16 | module.exports = bitbucketApi;
17 |
--------------------------------------------------------------------------------
/generators/bitrise/apis/bitriseApiConfig.js:
--------------------------------------------------------------------------------
1 | const apisauce = require('apisauce');
2 |
3 | const baseURL = 'https://api.bitrise.io/v0.1';
4 |
5 | const bitriseApi = apisauce.create({
6 | baseURL,
7 | timeout: 30000
8 | });
9 |
10 | bitriseApi.addMonitor(response => {
11 | if (!response.ok) {
12 | console.log(response.data);
13 | }
14 | });
15 |
16 | module.exports = bitriseApi;
17 |
--------------------------------------------------------------------------------
/generators/bitrise/apis/githubApiConfig.js:
--------------------------------------------------------------------------------
1 | const apisauce = require('apisauce');
2 |
3 | const baseURL = 'https://api.github.com';
4 |
5 | const githubApi = apisauce.create({
6 | baseURL,
7 | timeout: 30000
8 | });
9 |
10 | githubApi.addMonitor(response => {
11 | if (!response.ok) {
12 | console.log(response.data);
13 | }
14 | });
15 |
16 | module.exports = githubApi;
17 |
--------------------------------------------------------------------------------
/generators/bitrise/apis/gitlabApiConfig.js:
--------------------------------------------------------------------------------
1 | const apisauce = require('apisauce');
2 |
3 | const baseURL = 'https://gitlab.com/api/v4';
4 |
5 | const gitlabApi = apisauce.create({
6 | baseURL,
7 | timeout: 30000
8 | });
9 |
10 | gitlabApi.addMonitor(response => {
11 | if (!response.ok) {
12 | console.log(response.data);
13 | }
14 | });
15 |
16 | module.exports = gitlabApi;
17 |
--------------------------------------------------------------------------------
/generators/bitrise/bitrise.js:
--------------------------------------------------------------------------------
1 | const Generator = require('yeoman-generator');
2 |
3 | const completeREADME = require('./tasks/completeREADME');
4 | const bitriseInitialization = require('./tasks/bitriseInitialization');
5 | const nextSteps = require('./tasks/nextSteps');
6 | const loadBitriseInfo = require('./bitriseUtils');
7 |
8 | const BITRISE_YML = 'bitrise.yml';
9 |
10 | class BitriseInit extends Generator {
11 | async loadInfo() {
12 | const configInfo = await loadBitriseInfo.bind(this)();
13 | this.configInfo = configInfo;
14 | }
15 |
16 | constructor(args, opts) {
17 | super(args, opts);
18 |
19 | this.option('verbose', {
20 | desc: 'Turns on verbose logging',
21 | alias: 'v',
22 | type: Boolean,
23 | default: false
24 | });
25 |
26 | this.conflicter.force = true;
27 | this.features = {
28 | bitrise: true
29 | };
30 | }
31 |
32 | install() {
33 | return Promise.resolve().then(() => bitriseInitialization.bind(this)());
34 | }
35 |
36 | writing() {
37 | if (this.configInfo && this.configInfo.projectName && this.configInfo.projectPath) {
38 | const filepathWithoutExtension = BITRISE_YML.substring(0, BITRISE_YML.lastIndexOf('.'));
39 | const templatePath = `${filepathWithoutExtension}.ejs`;
40 |
41 | this.fs.copyTpl(
42 | this.templatePath(...templatePath.split('/')),
43 | this.destinationPath(...this.configInfo.projectPath.concat('/bitrise.yml').split('/')),
44 | {
45 | projectName: this.configInfo.projectName
46 | }
47 | );
48 | }
49 | }
50 |
51 | end() {
52 | completeREADME.bind(this)();
53 | nextSteps.bind(this)();
54 | }
55 | }
56 |
57 | module.exports = BitriseInit;
58 |
--------------------------------------------------------------------------------
/generators/bitrise/bitriseInfo.json:
--------------------------------------------------------------------------------
1 | {
2 | "repositoryUrlSsh": "",
3 | "publicApp": false,
4 | "repositorySlug": "",
5 | "repoOwner": "",
6 | "gitProvider": "",
7 | "gitToken": "",
8 | "bitriseToken": "",
9 | "bitriseOrganizationSlug": "",
10 | "projectPath": "",
11 | "projectName": ""
12 | }
13 |
--------------------------------------------------------------------------------
/generators/bitrise/bitriseREADME.md:
--------------------------------------------------------------------------------
1 |
2 | # Bitrise
3 |
4 | This documentation shows how to integrate your React Native project with [Bitrise](https://bitrise.io).
5 |
6 | ## Prerequisites
7 |
8 | - You need access to a [Bitrise](https://bitrise.io) account
9 |
10 | ## Convert secret variables to base64:
11 |
12 | All the files that are necessary to deploy:
13 |
14 | - `.env` files
15 | - Google services files for both Android and iOS
16 | - Android keystore
17 | - Android Gradle properties
18 |
19 | These files need to be stored in environment variables in Bitrise.
20 |
21 | For this process you need to perform some steps from the terminal in bash.
22 |
23 | In the next example, we will move the .env files to Bitrise
24 |
25 | ### 1) Convert .env files to base64:
26 |
27 | Go to the root folder of your project and run this command:
28 |
29 | ```bash
30 | tar -czvf /tmp/environment.tar.gz .*.env && base64 /tmp/environment.tar.gz
31 | ```
32 |
33 | This command will convert all the .env files of the project and print something like this:
34 |
35 | ```
36 | a .dev.env
37 | a .production.env
38 | a .stage.env
39 |
40 | H4sIAB2Rcl4AA-0SwU7DMAyGd+5TRLnTpavpULVuPAAnXmAyabpEa5MqcTd4e7K2EJAY0pAQQup3cWL9dmL5B1M5qysVXce8E6wFbZiTnjXwYnsMoTdC7bxwUpr4uW0W18M5z7OMnOMqz4fIl+N9IMs4SdW8TVd5dpsnhC/TLE0WhP/gravpPYILX9//EeDGovL+mCrK6/6TQt7jP2G9DTslR+m8tqakScwpkUbYSpt9SXusb+7odhOtH2UDqI/yYbAFCUXGFzCap6QKsSsY80LJFnw85WNh22Csw+CnKUcjQqZjYZ2WBkPb88vhC6gFNB8Fowd3J12hKmkLGJzYgQtFX6iU1HuFl2VPIA57Z3tTlYTev2WFbaxjJ6VR0jAm+zznJvrr/czMzMz8Fq/N2GtmAAgdAA==
41 | ```
42 |
43 | Copy the code, well'll need it in the next step.
44 |
45 | To convert other files, replace `.*.env` with the files you want to convert.
46 |
47 | \*In case the command doesn't print anything, check if you have `base64` installed.
48 |
49 | ### 2) Export the code to a Bitrise Secret variable:
50 |
51 | 1. Go to your project in [bitrise.io](https://bitrise.io)
52 | 2. In the Secrets tab add a base64 variable named `environment_tar_gz`
53 | 3. Paste the code created in Step 1
54 | 4. Save changes
55 |
56 | ### 3) Extract the secret variable in the bitrise script (bitrise.yml):
57 |
58 | We need to extract the `environment_tar_gz` variable in the building process, so we add this in the build script
59 |
60 | ```bash
61 | cd ${DIRECTORY_TO_EXTRACT}
62 | echo ${environment_tar_gz} | base64 -D | tar -xz
63 | ```
64 |
65 | Or we can use this function in bash:
66 |
67 | ```bash
68 | function extract_base64 () {
69 | cd $2
70 | echo $1 | base64 -D | tar -xz
71 | }
72 |
73 | extract_base64 ${environment_tar_gz} ${DIRECTORY_TO_EXTRACT}
74 | ```
75 |
76 | In our example, we need to extract the .env files in the root folder, so we add
77 |
78 | `extract_base64 ${environment_tar_gz} $HOME/git/`
79 |
80 | ## Deploy using Bitrise:
81 |
82 | To deploy a new version of the app in Android or iOS we need to create a tag from a specific branch.
83 |
84 | Example of how to create a deploy to the development environment:
85 |
86 | 1. Move to the `development` branch:
87 | 2. Set variables for the device, environment and version of the new build
88 | 3. Create a new tag using the variables
89 | 4. Push the new tag
90 |
91 | ```bash
92 | git checkout development
93 |
94 | git pull origin development
95 |
96 | DEVICE=android
97 |
98 | ENVIRONMENT=qa
99 |
100 | VERSION=1.0.0
101 |
102 | FEATURE=`feature name (e.g., "login")`
103 |
104 | git tag -a $DEVICE-$ENVIRONMENT-$VERSION-$FEATURE -m "Comment about the release" && git push origin $DEVICE-$ENVIRONMENT-$VERSION-$FEATURE
105 | ```
106 |
107 | Or directly:
108 |
109 | ```bash
110 | git checkout development
111 |
112 | git pull origin development
113 |
114 | git tag -a android-qa-1.0.0-login -m "Comment about the release" && git push origin android-qa-1.0.0-login
115 | ```
116 |
117 | ### Possible values:
118 |
119 | These are the supported values for each variable of the TAG:
120 |
121 | - DEVICE: `android` or `ios`
122 |
123 | - ENVIRONMENT: `qa` `stage` or `prod`
124 |
125 | - VERSION: Given a version number MAJOR.MINOR.PATCH, you should increment:
126 |
127 | 1. MAJOR when you make incompatible API changes.
128 | 2. MINOR when you add functionality in a backwards-compatible manner.
129 | 3. PATCH when you make backwards-compatible bug fixes or small changes
130 |
131 | - FEATURE: name of the feature or release that you are going to deploy
132 |
133 | ---
134 |
--------------------------------------------------------------------------------
/generators/bitrise/bitriseUtils.js:
--------------------------------------------------------------------------------
1 | const { BITRISE_PROMPTS } = require('./constants');
2 |
3 | function buildAllPrompts(context) {
4 | return context.prompt(Object.keys(BITRISE_PROMPTS).map(key => BITRISE_PROMPTS[key]));
5 | }
6 |
7 | function buildSomePrompts(keys, context) {
8 | return context.prompt(keys.map(key => BITRISE_PROMPTS[key]));
9 | }
10 |
11 | function isNotEmpty(key, value) {
12 | return (
13 | ((!value && typeof value === 'string') || value === null || value === undefined) &&
14 | key !== 'projectPath' &&
15 | key !== 'projectName'
16 | );
17 | }
18 |
19 | function buildMessage(keys) {
20 | let lastMessage = '';
21 | keys.forEach(
22 | key =>
23 | (lastMessage = lastMessage.concat(
24 | '\n',
25 | `The field ${key} in bitriseInfo.json file is required to run the script`
26 | ))
27 | );
28 | return lastMessage;
29 | }
30 |
31 | function validateConfigObject(object, context) {
32 | let keys = [];
33 | if (!object) {
34 | return 'The bitriseInfo.json file is written wrong';
35 | }
36 | Object.keys(object).forEach(key => {
37 | if (isNotEmpty(key, object[key])) {
38 | keys = [...keys, key];
39 | }
40 | });
41 | const lastMessage = buildMessage(keys);
42 | console.log(lastMessage.red.bold.underline);
43 | return buildSomePrompts(keys, context);
44 | }
45 |
46 | async function loadBitriseInfoFile(context) {
47 | let configInfo = null;
48 | try {
49 | configInfo = require('./bitriseInfo.json');
50 | } catch (e) {
51 | console.log('The bitriseInfo.json file is written wrong'.red.underline.bold);
52 | configInfo = await buildAllPrompts(context);
53 | }
54 | return configInfo;
55 | }
56 |
57 | async function loadBitriseInfo() {
58 | const configInfo = await loadBitriseInfoFile(this);
59 | const lastConfig = await validateConfigObject(configInfo, this);
60 | const newConfig = {
61 | ...configInfo,
62 | ...lastConfig
63 | };
64 | return newConfig;
65 | }
66 |
67 | module.exports = loadBitriseInfo;
68 |
--------------------------------------------------------------------------------
/generators/bitrise/constants.js:
--------------------------------------------------------------------------------
1 | module.exports.BITRISE_PROMPTS = {
2 | repositoryUrlSsh: {
3 | type: 'input',
4 | name: 'repositoryUrlSsh',
5 | message: "What's your repository url? (ssh only)",
6 | validate: val => (val ? true : 'Repository url (ssh) is required to configure bitrise')
7 | },
8 | publicApp: {
9 | type: 'input',
10 | name: 'publicApp',
11 | message:
12 | 'Is your repo public? If true then the repository visibility setting will be public, in case of false it will be private',
13 | validate: val => (typeof val === 'boolean' ? true : 'This value is required to configure bitrise'),
14 | filter: val => val === 'true'
15 | },
16 | repositorySlug: {
17 | type: 'input',
18 | name: 'repositorySlug',
19 | message: 'Write the repo slug (The name of your repo not the url)',
20 | validate: val => (val ? true : 'Repository slug is required to configure bitrise')
21 | },
22 | repoOwner: {
23 | type: 'input',
24 | name: 'repoOwner',
25 | message: 'Who is the owner of the repo?',
26 | validate: val => (val ? true : 'Owner is required to configure bitrise')
27 | },
28 | gitProvider: {
29 | type: 'input',
30 | name: 'gitProvider',
31 | message:
32 | "The git provider you are using, it can be 'github', 'bitbucket', 'gitlab', 'gitlab-self-hosted' or 'custom'",
33 | validate: val => (val ? true : 'Provider is required to configure bitrise')
34 | },
35 | gitToken: {
36 | type: 'input',
37 | name: 'gitToken',
38 | message:
39 | "Please, write your git token (github, gitlab ot bitbucket) with permissions to create ssh keys here (write it with the format 'token ' if it is github, 'Bearer ' if it's gitlab or bitbucket)",
40 | validate: val => (val ? true : 'Github token is required to configure bitrise')
41 | },
42 | bitriseOrganizationSlug: {
43 | type: 'input',
44 | name: 'bitriseOrganizationSlug',
45 | message: 'Please, write your Bitrise organization slug',
46 | validate: val => (val ? true : 'Organization slug is required')
47 | },
48 | bitriseToken: {
49 | type: 'input',
50 | name: 'bitriseToken',
51 | message: 'Please, write your bitrise token with permissions to create ssh keys here',
52 | validate: val => (val ? true : 'Bitrise token is required to configure bitrise')
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/generators/bitrise/defaultBitrise.yml:
--------------------------------------------------------------------------------
1 | ---
2 | format_version: 1.4.0
3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
4 | trigger_map:
5 | - pull_request_source_branch: "*"
6 | workflow: primary
7 | workflows:
8 | _run_from_repo:
9 | steps:
10 | - activate-ssh-key:
11 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
12 | - git-clone: {}
13 | - script:
14 | title: continue from repo
15 | inputs:
16 | - content: |-
17 | #!/bin/bash
18 | set -ex
19 | bitrise run "${BITRISE_TRIGGERED_WORKFLOW_ID}"
20 | primary:
21 | after_run:
22 | - _run_from_repo
23 |
24 |
--------------------------------------------------------------------------------
/generators/bitrise/tasks/bitriseInitialization.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const runCommand = require('../../app/tasks/runCommand');
4 | const loadBitriseInfo = require('../bitriseUtils');
5 |
6 | const createBitriseApp = require('./createBitriseApp');
7 |
8 | module.exports = function bitriseInitialization() {
9 | return runCommand({
10 | command: ['ssh-keygen', ['-t', 'rsa', '-b', '4096', '-P', '', '-f', './bitrise-ssh', '-m', 'PEM']],
11 | loadingMessage: 'Creating ssh key...',
12 | context: this.options
13 | }).then(async ({ spinner }) => {
14 | spinner.stop();
15 | const privateSshKey = fs.readFileSync('./bitrise-ssh').toString();
16 | const publicSshKey = fs.readFileSync('./bitrise-ssh.pub').toString();
17 | const bitriseYml = fs.readFileSync(`${this.templatePath()}/../../bitrise/defaultBitrise.yml`).toString();
18 | let configInfo = null;
19 | if (this.configInfo) {
20 | const { configInfo: info } = this;
21 | configInfo = info;
22 | } else {
23 | const info = await loadBitriseInfo.bind(this)();
24 | configInfo = info;
25 | }
26 | const values = {
27 | repoUrl: configInfo.repositoryUrlSsh,
28 | isPublic: configInfo.publicApp,
29 | repoSlug: configInfo.repositorySlug,
30 | gitOwner: configInfo.repoOwner,
31 | provider: configInfo.gitProvider,
32 | gitToken: configInfo.gitToken,
33 | bitriseOrganizationSlug: configInfo.bitriseOrganizationSlug,
34 | bitriseToken: configInfo.bitriseToken,
35 | type: 'git',
36 | privateSshKey,
37 | publicSshKey,
38 | bitriseYml
39 | };
40 | await createBitriseApp(values);
41 | });
42 | };
43 |
--------------------------------------------------------------------------------
/generators/bitrise/tasks/completeREADME.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | module.exports = function completeREADME() {
4 | const localReadme = fs.readFileSync(`${this.templatePath()}/../../bitrise/bitriseREADME.md`).toString();
5 | const readmeFile = fs.readFileSync(`${this.configInfo.projectPath}/README.md`).toString();
6 | fs.writeFileSync(`${this.configInfo.projectPath}/README.md`, readmeFile.concat(localReadme));
7 | };
8 |
--------------------------------------------------------------------------------
/generators/bitrise/tasks/createBitriseApp.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | const bitriseApi = require('../apis/bitriseApiConfig');
3 | const githubApi = require('../apis/githubApiConfig');
4 | const gitlabApi = require('../apis/gitlabApiConfig');
5 | const bitbucketApi = require('../apis/bitbucketApiConfig');
6 |
7 | const gitApi = {
8 | github: githubApi,
9 | gitlab: gitlabApi,
10 | bitbucket: bitbucketApi
11 | };
12 |
13 | const createApp = ({ repoUrl, isPublic, gitOwner, provider, repoSlug, type, bitriseOrganizationSlug }) =>
14 | bitriseApi.post('/apps/register', {
15 | git_owner: gitOwner,
16 | is_public: isPublic,
17 | provider,
18 | repo_url: repoUrl,
19 | git_repo_slug: repoSlug,
20 | type,
21 | organization_slug: bitriseOrganizationSlug
22 | });
23 |
24 | const createSshOnBitbucket = async (repoSlug, publicSshKey) => {
25 | const userResponse = await gitApi.bitbucket.get('/user/keys');
26 | const user = userResponse.data.username;
27 | return gitApi.bitbucket.post(`/users/${user}/ssh-keys`, {
28 | label: `bitrise-${repoSlug}`,
29 | key: publicSshKey
30 | });
31 | };
32 |
33 | const createSshGit = ({ repoSlug, publicSshKey, provider }) =>
34 | // IMPORTANT -> GITLAB AND GITHUB HAVE THE SAME EP
35 | provider === 'bitbucket'
36 | ? createSshOnBitbucket(repoSlug, publicSshKey)
37 | : gitApi[provider].post('/user/keys', {
38 | title: `bitrise-${repoSlug}`,
39 | key: publicSshKey
40 | });
41 |
42 | const registerSshKeyOnBitrise = ({ slug, publicSshKey, privateSshKey }) =>
43 | bitriseApi.post(`/apps/${slug}/register-ssh-key`, {
44 | auth_ssh_private_key: privateSshKey,
45 | auth_ssh_public_key: publicSshKey,
46 | // eslint-disable-next-line id-length
47 | is_register_key_into_provider_service: false
48 | });
49 |
50 | const finishBitrise = ({ slug, bitriseOrganizationSlug }) =>
51 | bitriseApi.post(`/apps/${slug}/finish`, {
52 | config: 'default-react-native-config',
53 | envs: {},
54 | mode: 'manual',
55 | project_type: 'react-native',
56 | stack_id: 'osx-xcode-11.4.x',
57 | organization_slug: bitriseOrganizationSlug
58 | });
59 |
60 | const loadYmlToBitrise = ({ slug, bitriseYml }) =>
61 | bitriseApi.post(`/apps/${slug}/bitrise.yml`, {
62 | app_config_datastore_yaml: bitriseYml
63 | });
64 |
65 | const loadWebHook = ({ slug }) => bitriseApi.post(`/apps/${slug}/register-webhook`);
66 |
67 | const setAuthenticationHeaders = ({ gitToken, bitriseToken, provider }) => {
68 | bitriseApi.setHeaders({
69 | Authorization: bitriseToken
70 | });
71 | gitApi[provider].setHeaders({
72 | Authorization: gitToken
73 | });
74 | };
75 |
76 | module.exports = async function createBitriseApp(values) {
77 | setAuthenticationHeaders(values);
78 | console.log('creating bitrise app...'.green.bold);
79 | const slugData = await createApp(values);
80 | console.log('Registering ssh key on github...'.green.bold);
81 | await createSshGit(values);
82 | const { slug } = slugData.data;
83 | const newValues = {
84 | ...values,
85 | slug
86 | };
87 | console.log('Registering ssh key on bitrise...'.green.bold);
88 | await registerSshKeyOnBitrise(newValues);
89 | console.log('last step to create bitrise api...'.green.bold);
90 | await finishBitrise(newValues);
91 | console.log('Loading yml file to bitrise...'.green.bold);
92 | await loadYmlToBitrise(newValues);
93 | console.log('adding webhook to github...'.green.bold);
94 | await loadWebHook(newValues);
95 | };
96 |
--------------------------------------------------------------------------------
/generators/bitrise/tasks/nextSteps.js:
--------------------------------------------------------------------------------
1 | require('colors');
2 |
3 | module.exports = function nextSteps() {
4 | console.log(`\n ${'NEXT STEPS!'.bold.underline.white} \n`);
5 | console.log(
6 | 'Remember to check this folder and look after the private and public key generated for Bitrise'.bold
7 | .underline.red
8 | );
9 | console.log('Move those files to a safe place and remove them from this folder.'.bold.underline.red);
10 | console.log('\n\n');
11 | };
12 |
--------------------------------------------------------------------------------
/generators/bitrise/templates/bitrise.ejs:
--------------------------------------------------------------------------------
1 | ---
2 | format_version: '8'
3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
4 | project_type: react-native
5 | trigger_map:
6 | - pull_request_source_branch: "*"
7 | workflow: primary
8 | workflows:
9 | deploy:
10 | description:
11 | "## Configure Android part of the deploy workflow\n\nTo generate
12 | a signed APK:\n\n1. Open the **Workflow** tab of your project on Bitrise.io\n1.
13 | Add **Sign APK step right after Android Build step**\n1. Click on **Code Signing**
14 | tab\n1. Find the **ANDROID KEYSTORE FILE** section\n1. Click or drop your file
15 | on the upload file field\n1. Fill the displayed 3 input fields:\n1. **Keystore
16 | password**\n1. **Keystore alias**\n1. **Private key password**\n1. Click on
17 | **[Save metadata]** button\n\nThat's it! From now on, **Sign APK** step will
18 | receive your uploaded files.\n\n## Configure iOS part of the deploy workflow\n\nTo
19 | generate IPA:\n\n1. Open the **Workflow** tab of your project on Bitrise.io\n1.
20 | Click on **Code Signing** tab\n1. Find the **PROVISIONING PROFILE** section\n1.
21 | Click or drop your file on the upload file field\n1. Find the **CODE SIGNING
22 | IDENTITY** section\n1. Click or drop your file on the upload file field\n1.
23 | Click on **Workflows** tab\n1. Select deploy workflow\n1. Select **Xcode Archive
24 | & Export for iOS** step\n1. Open **Force Build Settings** input group\n1. Specify
25 | codesign settings\nSet **Force code signing with Development Team**, **Force
26 | code signing with Code Signing Identity** \nand **Force code signing with Provisioning
27 | Profile** inputs regarding to the uploaded codesigning files\n1. Specify manual
28 | codesign style\nIf the codesigning files, are generated manually on the Apple
29 | Developer Portal, \nyou need to explicitly specify to use manual coedsign settings
30 | \ \n(as ejected rn projects have xcode managed codesigning turned on). \nTo
31 | do so, add 'CODE_SIGN_STYLE=\"Manual\"' to 'Additional options for xcodebuild
32 | call' input\n\n## To run this workflow\n\nIf you want to run this workflow manually:\n\n1.
33 | Open the app's build list page\n2. Click on **[Start/Schedule a Build]** button\n3.
34 | Select **deploy** in **Workflow** dropdown input\n4. Click **[Start Build]**
35 | button\n\nOr if you need this workflow to be started by a GIT event:\n\n1. Click
36 | on **Triggers** tab\n2. Setup your desired event (push/tag/pull) and select
37 | **deploy** workflow\n3. Click on **[Done]** and then **[Save]** buttons\n\nThe
38 | next change in your repository that matches any of your trigger map event will
39 | start **deploy** workflow.\n"
40 | steps:
41 | - activate-ssh-key:
42 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
43 | - git-clone: {}
44 | - script:
45 | title: Do anything with Script step
46 | - yarn:
47 | inputs:
48 | - command: install
49 | before_run: []
50 | primary:
51 | steps:
52 | - activate-ssh-key:
53 | run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
54 | - git-clone: {}
55 | - yarn:
56 | inputs:
57 | - command: install
58 | - yarn:
59 | inputs:
60 | - command: run lint
61 | - yarn:
62 | inputs:
63 | - command: run check-types
64 | - yarn:
65 | inputs:
66 | - command: test
67 | app:
68 | envs:
69 | - PROJECT_LOCATION: android
70 | - MODULE: app
71 | - VARIANT: release
72 | - BITRISE_PROJECT_PATH: ios/<%= projectName %>.xcworkspace
73 | - BITRISE_SCHEME: <%= projectName %>
74 | - BITRISE_EXPORT_METHOD: ad-hoc
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "generator-wolmo-bootstrap-rn",
3 | "version": "1.9.0",
4 | "description": "A project generator for react native applications with some boilerplates already configured and ready to use.",
5 | "files": [
6 | "generators"
7 | ],
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/Wolox/wolmo-bootstrap-react-native.git"
11 | },
12 | "keywords": [
13 | "yeoman-generator",
14 | "react-native"
15 | ],
16 | "author": "Wolox",
17 | "license": "MIT",
18 | "bugs": {
19 | "url": "https://github.com/Wolox/wolmo-bootstrap-react-native/issues"
20 | },
21 | "homepage": "https://github.com/Wolox/wolmo-bootstrap-react-native#readme",
22 | "scripts": {
23 | "lint": "eslint .",
24 | "lint-diff": "git diff --staged --name-only --relative --diff-filter=ACM | grep -E \"\\.(ts|tsx|js|jsx)$\" | xargs eslint",
25 | "lint-fix": "eslint . --fix"
26 | },
27 | "dependencies": {
28 | "acorn": "^7.1.1",
29 | "apisauce": "^2.0.1",
30 | "colors": "^1.1.2",
31 | "deep-extend": "^0.5.1",
32 | "diff": "^3.5.0",
33 | "fs": "0.0.1-security",
34 | "latest-semver": "^1.0.0",
35 | "minimist": "^1.2.5",
36 | "ora": "^1.1.0",
37 | "semver-regex": "^1.0.0",
38 | "yeoman-generator": "^4.13.0"
39 | },
40 | "devDependencies": {
41 | "@babel/core": "^7.9.0",
42 | "@babel/runtime": "^7.9.2",
43 | "@typescript-eslint/eslint-plugin": "^2.27.0",
44 | "babel-eslint": "^10.1.0",
45 | "eslint": "^6.8.0",
46 | "eslint-config-airbnb": "^18.1.0",
47 | "eslint-config-prettier": "^6.10.1",
48 | "eslint-config-wolox-react-native": "^1.0.1",
49 | "eslint-plugin-babel": "^5.3.0",
50 | "eslint-plugin-eslint-comments": "^3.1.2",
51 | "eslint-plugin-flowtype": "^4.7.0",
52 | "eslint-plugin-import": "^2.20.1",
53 | "eslint-plugin-jest": "^23.8.2",
54 | "eslint-plugin-jsx-a11y": "^6.2.3",
55 | "eslint-plugin-prettier": "^3.1.2",
56 | "eslint-plugin-react": "^7.19.0",
57 | "eslint-plugin-react-hooks": "^2.5.0",
58 | "eslint-plugin-react-native": "^3.8.1",
59 | "husky": "^4.2.5",
60 | "prettier": "^2.0.4",
61 | "prettier-eslint": "^9.0.1"
62 | },
63 | "husky": {
64 | "hooks": {
65 | "pre-commit": "yarn run lint-diff"
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------