├── .gitignore ├── App.js ├── LICENSE ├── README.md ├── app.json ├── app ├── actions │ ├── auth │ │ ├── auth.ts │ │ └── types.ts │ ├── filters │ │ ├── filters.ts │ │ └── types.ts │ └── todos │ │ ├── todos.test.ts │ │ ├── todos.ts │ │ └── types.ts ├── app.tsx ├── components │ ├── button │ │ ├── button.props.ts │ │ ├── button.story.tsx │ │ └── button.tsx │ ├── category-button │ │ ├── category-button.props.ts │ │ └── category-button.tsx │ ├── checkbox │ │ ├── checkbox.props.ts │ │ ├── checkbox.story.tsx │ │ └── checkbox.tsx │ ├── error-message │ │ ├── error-message.props.ts │ │ ├── error-message.story.tsx │ │ └── error-message.tsx │ ├── filters-form │ │ ├── filters-form.props.ts │ │ └── filters-form.tsx │ ├── header │ │ ├── header.props.ts │ │ └── header.tsx │ ├── heading │ │ ├── heading.props.ts │ │ ├── heading.story.tsx │ │ └── heading.tsx │ ├── index.ts │ ├── input │ │ ├── input.props.ts │ │ ├── input.story.tsx │ │ └── input.tsx │ ├── loading-button │ │ ├── loading-button.props.ts │ │ └── loading-button.tsx │ ├── new-todo-form │ │ ├── new-todo-form.props.ts │ │ └── new-todo-form.tsx │ ├── reactive-theme-provider │ │ ├── reactive-theme-provider.props.ts │ │ └── reactive-theme-provider.tsx │ ├── screen │ │ ├── screen.props.ts │ │ └── screen.tsx │ ├── todo-item │ │ ├── todo-item.props.ts │ │ ├── todo-item.story.tsx │ │ └── todo-item.tsx │ └── todo-list │ │ ├── todo-list.props.ts │ │ └── todo-list.tsx ├── firebase │ ├── firebase.ts │ └── index.ts ├── hooks │ ├── index.ts │ ├── useCredentialsFields.ts │ ├── useIsFirstRoute.ts │ └── useReactiveTheme.ts ├── i18n │ ├── en.json │ ├── he.json │ ├── i18n.ts │ ├── index.ts │ └── translate.ts ├── navigators │ ├── auth-navigator.tsx │ ├── index.ts │ ├── main-navigator.tsx │ ├── navigation-utilities.tsx │ └── root-navigator.tsx ├── reducers │ ├── auth │ │ └── auth.ts │ ├── filters │ │ └── filters.ts │ └── todos │ │ ├── todos.test.ts │ │ └── todos.ts ├── screens │ ├── home │ │ ├── home-screen.tsx │ │ └── home.props.ts │ ├── index.ts │ ├── sign-in-with-email │ │ ├── sign-in-with-email-screen.tsx │ │ └── sign-in-with-email.ts │ ├── sign-up-with-email │ │ ├── sign-up-with-email-screen.tsx │ │ └── sign-up-with-email.ts │ └── welcome │ │ ├── welcome-screen.tsx │ │ └── welcome.ts ├── selectors │ └── todos.ts ├── services │ ├── auth-api.ts │ └── todos-api.ts ├── store │ ├── configureStore.js │ ├── middlewares.js │ └── rootReducer.js ├── test │ └── fixtures │ │ ├── index.ts │ │ └── todos.ts ├── theme │ ├── colors.ts │ ├── fonts │ │ └── index.ts │ ├── index.ts │ ├── palette.ts │ ├── spacing.ts │ ├── themes.ts │ ├── timing.ts │ └── typography.ts ├── types │ ├── auth.ts │ ├── colors.ts │ ├── emotion.d.ts │ ├── env.d.ts │ ├── filters.ts │ ├── index.ts │ ├── palette.ts │ ├── theme.ts │ └── todo.ts └── utils │ ├── delay.ts │ ├── ignore-warnings.ts │ ├── keychain.ts │ ├── storage │ ├── index.ts │ └── storage.ts │ ├── to.ts │ └── validate.ts ├── assets ├── fonts │ ├── Merriweather-Bold.ttf │ └── Merriweather-Regular.ttf └── images │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png ├── babel.config.js ├── ignite └── templates │ ├── action │ ├── NAME.test.ts.ejs │ ├── NAME.ts.ejs │ └── types.ts.ejs │ ├── component │ ├── NAME.props.ts.ejs │ ├── NAME.story.tsx.ejs │ └── NAME.tsx.ejs │ ├── navigator │ └── NAME-navigator.tsx.ejs │ ├── reducer │ └── NAME.ts.ejs │ └── screen │ ├── NAME-screen.tsx.ejs │ └── NAME.props.ts.ejs ├── package-lock.json ├── package.expo.json ├── package.json ├── react-native.config.js ├── storybook ├── index.ts ├── storybook-registry.ts ├── storybook.tsx └── views │ ├── docs.tsx │ ├── index.ts │ ├── story-screen.tsx │ ├── story.tsx │ └── use-case.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # Android/IntelliJ 25 | # 26 | build/ 27 | .idea 28 | .gradle 29 | local.properties 30 | *.iml 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | # BUCK 39 | buck-out/ 40 | \.buckd/ 41 | *.keystore 42 | !debug.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # CocoaPods 59 | /ios/Pods/ 60 | 61 | # Ignite-specific items below 62 | # You can safely replace everything above this comment with whatever is 63 | # in the default .gitignore generated by React-Native CLI 64 | 65 | # VS Code 66 | .vscode 67 | 68 | # Expo 69 | .expo/* 70 | bin/Exponent.app 71 | app.json 72 | 73 | npm-debug.* 74 | *.jks 75 | *.p8 76 | *.p12 77 | *.key 78 | *.mobileprovision 79 | *.orig.* 80 | web-build/ 81 | 82 | # Configurations 83 | app/config/env.*.js 84 | !env.js 85 | .env.development 86 | .env.production -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | // This is the first file that ReactNative will run when it starts up. 2 | // 3 | // We jump out of here immediately and into our main entry point instead. 4 | // 5 | // It is possible to have React Native load our main module first, but we'd have to 6 | // change that in both AppDelegate.m and MainApplication.java. This would have the 7 | // side effect of breaking other tooling like mobile-center and react-native-rename. 8 | // 9 | // It's easier just to leave it here. 10 | import app from "./app/app.tsx"; 11 | import { registerRootComponent } from "expo"; 12 | 13 | // Should we show storybook instead of our app? 14 | // 15 | // ⚠️ Leave this as `false` when checking into git. 16 | const SHOW_STORYBOOK = false; 17 | 18 | let RootComponent = app; 19 | if (__DEV__) { 20 | if (SHOW_STORYBOOK) { 21 | // Only include Storybook if we're in dev mode 22 | const { StorybookUIRoot } = require("./storybook"); 23 | RootComponent = StorybookUIRoot; 24 | } 25 | } 26 | 27 | registerRootComponent(RootComponent); 28 | export default RootComponent; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 One Flex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

logo

2 | 3 | # Regnite - React Native + Firebase boilerplate 4 | 5 | ## The famous Ignite boilerplate with extra batteries included 6 | 7 | [Ignite boilerplate ](https://github.com/infinitered/ignite) by Infinite Red is well known in the React Native community. It is a reliable battle-tested boilerplate to kickstart your React Native app. 8 | 9 | Here at oneflex, we extended the Ignite boilerplate to include firebase authentication and better support for Expo. 10 | 11 | ## Tech Stack 12 | 13 | Regnite uses the Ignite tech stack with some additions and modifications: 14 | 15 | - Expo 16 | - TypeScript 17 | - Firebase 18 | - React Navigation 5 19 | - Redux [(Why Redux?)](https://github.com/oneflex/regnite#why-redux) 20 | - Redux Thunk and Redux Persist 21 | - Emotion for reactive theming 22 | - React Native Debugger 23 | - Storybook [(Usage)](https://github.com/oneflex/regnite#storybook-usage) 24 | - i18n 25 | 26 | ## Quick Start 27 | 28 | Clone the boilerplate: 29 | 30 | ```bash 31 | git clone https://github.com/oneflex/regnite.git my-app 32 | cd my-app && npm i 33 | ``` 34 | 35 | Create a Firebase project [(How?)](https://firebase.google.com/docs/web/setup), add a web app, and copy your Firebase config to a `.env.development` file in the top level of your project: 36 | 37 | ```.env 38 | FIREBASE_API_KEY=[...] 39 | FIREBASE_AUTH_DOMAIN=[...] 40 | FIREBASE_DATABASE_URL=[...] 41 | FIREBASE_PROJECT_ID=[...] 42 | FIREBASE_STORAGE_BUCKET=[...] 43 | FIREBASE_MESSENAGING_SENDER_ID=[...] 44 | FIREBASE_APP_ID=[...] 45 | ``` 46 | 47 | If you want to use [Google](https://docs.expo.dev/versions/latest/sdk/google/) and [Facebook](https://docs.expo.dev/versions/latest/sdk/facebook/) authentication, include the respective auth keys from these platforms in the `.env.development` file: 48 | 49 | ```.env 50 | FACEBOOK_APP_ID=[...] 51 | GOOGLE_IOS_CLIENT_ID=[...] 52 | GOOGLE_ANDROID_CLIENT_ID=[...] 53 | ``` 54 | 55 | Note that Facebook login doesn't work in the Expo Go app yet. 56 | 57 | ## Generators 58 | 59 | We use [Ignite Generators](https://github.com/infinitered/ignite#generators) to quickly create screens, components etc. 60 | 61 | You can create your own, or use our ready made generators for: 62 | 63 | - components 64 | - screens 65 | - navigator 66 | - redux actions and reducers 67 | 68 | Using a generator: 69 | 70 | ``` 71 | npm run g component TodoItem 72 | npm run g screen Home 73 | npm run g action auth 74 | ``` 75 | 76 | ## Storybook Usage 77 | 78 | To register a component in storybook, simply require it in the `storybook-registry.ts` file: 79 | 80 | ```ts 81 | require("../app/components/heading/heading.story"); 82 | ``` 83 | 84 | Expo doesn't yet allow adding options to the developer menu, so in order to launch storybook you need to change the `SHOW_STORYBOOK` variable in `App.js` to true: 85 | 86 | ```js 87 | // Should we show storybook instead of our app? 88 | // 89 | // ⚠️ Leave this as `false` when checking into git. 90 | const SHOW_STORYBOOK = true; 91 | ``` 92 | 93 | ## Why Redux? 94 | 95 | You may have noticed we use Redux instead of MST (Ignite choice of state management). Redux was a requirement for the project that kickstarted this boilerplate, so we replaced MST with Redux. 96 | 97 | We plan on publishing an alternative Regnite boilerplate with [Zustand](https://github.com/pmndrs/zustand) instead of Redux in the future. Any contributions will be highly appreciated! 98 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "regnite-todo", 4 | "slug": "regnite-todo", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "splash": { 9 | "image": "./assets/images/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "userInterfaceStyle": "automatic", 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "userInterfaceStyle": "automatic" 25 | }, 26 | "web": { 27 | "favicon": "./assets/images/favicon.png" 28 | }, 29 | "userInterfaceStyle": "automatic", 30 | "facebookScheme": "fb1553878008291137", 31 | "facebookAppId": "1553878008291137", 32 | "facebookDisplayName": "regnite-todo", 33 | "facebookAutoInitEnabled": true 34 | } 35 | } -------------------------------------------------------------------------------- /app/actions/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import { to } from "../../utils/to"; 2 | import { Status, Error } from "../../types/auth"; 3 | import * as authApi from "../../services/auth-api"; 4 | 5 | export const updateStatus = (status: Status) => ({ 6 | type: "UPDATE_STATUS", 7 | payload: { 8 | status, 9 | }, 10 | }); 11 | 12 | export const updateError = (error: Error) => ({ 13 | type: "UPDATE_ERROR", 14 | payload: { 15 | error, 16 | }, 17 | }); 18 | 19 | export const signIn = (user: any) => ({ 20 | type: "SIGN_IN", 21 | payload: { 22 | user, 23 | }, 24 | }); 25 | 26 | export const startSignIn = (email: string, password: string) => { 27 | return async (dispatch: any) => { 28 | dispatch(updateStatus("loading")); 29 | 30 | const [error, userCredential] = await authApi.signInWithEmail( 31 | email, 32 | password, 33 | ); 34 | 35 | if (error) { 36 | dispatch(updateStatus("failed")); 37 | dispatch(updateError(error.message)); 38 | return; 39 | } 40 | 41 | dispatch(updateStatus("succeeded")); 42 | dispatch(updateError(null)); 43 | dispatch(signIn(userCredential.user)); 44 | dispatch(updateStatus("idle")); 45 | }; 46 | }; 47 | 48 | export const startSignUp = (email: string, password: string) => { 49 | return async (dispatch: any) => { 50 | dispatch(updateStatus("loading")); 51 | 52 | const [error, userCredential] = await authApi.signUpWithEmail( 53 | email, 54 | password, 55 | ); 56 | 57 | if (error) { 58 | dispatch(updateStatus("failed")); 59 | dispatch(updateError(error.message)); 60 | return; 61 | } 62 | 63 | dispatch(updateStatus("succeeded")); 64 | dispatch(updateError(null)); 65 | dispatch(signIn(userCredential.user)); 66 | dispatch(updateStatus("idle")); 67 | }; 68 | }; 69 | 70 | export const startSignInWithFacebook = () => { 71 | return async (dispatch: any) => { 72 | dispatch(updateStatus("loading")); 73 | 74 | const [fbError, token] = await to(authApi.getFacebookToken()); 75 | 76 | if (fbError) { 77 | if (fbError.message === "canceled") { 78 | return; 79 | } 80 | dispatch(updateStatus("failed")); 81 | dispatch(updateError(fbError.message)); 82 | return; 83 | } 84 | 85 | const [error, userCredential] = await authApi.signInWithFacebook(token); 86 | 87 | if (error) { 88 | dispatch(updateStatus("failed")); 89 | dispatch(updateError("Login with Facebook failed")); 90 | return; 91 | } 92 | 93 | dispatch(updateStatus("succeeded")); 94 | dispatch(updateError(null)); 95 | dispatch(signIn(userCredential.user)); 96 | dispatch(updateStatus("idle")); 97 | }; 98 | }; 99 | 100 | export const startSignInWithGoogle = () => { 101 | return async (dispatch: any) => { 102 | dispatch(updateStatus("loading")); 103 | 104 | const [googleError, accessToken] = await to(authApi.getGoogleToken()); 105 | 106 | if (googleError) { 107 | dispatch(updateStatus("failed")); 108 | dispatch(updateError("Login with Google failed")); 109 | return; 110 | } 111 | 112 | const [error, userCredential] = await authApi.signInWithGoogle(accessToken); 113 | 114 | if (error) { 115 | dispatch(updateStatus("failed")); 116 | dispatch(updateError(error.message)); 117 | return; 118 | } 119 | 120 | dispatch(updateStatus("succeeded")); 121 | dispatch(updateError(null)); 122 | dispatch(signIn(userCredential.user)); 123 | dispatch(updateStatus("idle")); 124 | }; 125 | }; 126 | 127 | export const startSignInAnonymously = () => { 128 | return async (dispatch: any) => { 129 | dispatch(updateStatus("loading")); 130 | 131 | const [error, userCredential] = await authApi.signInAnonymously(); 132 | 133 | if (error) { 134 | dispatch(updateStatus("failed")); 135 | dispatch(updateError(error.message)); 136 | return; 137 | } 138 | 139 | dispatch(updateStatus("succeeded")); 140 | dispatch(updateError(null)); 141 | dispatch(signIn(userCredential.user)); 142 | dispatch(updateStatus("idle")); 143 | }; 144 | }; 145 | 146 | export const signOut = () => ({ 147 | type: "SIGN_OUT", 148 | payload: {}, 149 | }); 150 | 151 | export const startSignOut = () => { 152 | return async (dispatch: any) => { 153 | authApi.signOut(); 154 | dispatch(signOut()); 155 | }; 156 | }; 157 | -------------------------------------------------------------------------------- /app/actions/auth/types.ts: -------------------------------------------------------------------------------- 1 | import { User, Status, Error } from "../../types/auth"; 2 | 3 | export interface Action { 4 | type: 5 | | "SIGN_IN" 6 | | "SIGN_OUT" 7 | | "UPDATE_STATUS" 8 | | "UPDATE_ERROR" 9 | | "UPDATE_USER_DATA"; 10 | payload?: { 11 | user?: User; 12 | status?: Status; 13 | error?: Error; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /app/actions/filters/filters.ts: -------------------------------------------------------------------------------- 1 | import { Category, Sort } from "../../types/filters"; 2 | 3 | export const updateFilterBy = (type: Category) => ({ 4 | type: "UPDATE_FILTER_BY", 5 | payload: { 6 | type, 7 | }, 8 | }); 9 | 10 | export const updateSortBy = (type: Sort) => ({ 11 | type: "UPDATE_SORT_BY", 12 | payload: { 13 | type, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /app/actions/filters/types.ts: -------------------------------------------------------------------------------- 1 | export interface Action { 2 | type: "UPDATE_FILTER_BY" | "UPDATE_SORT_BY"; 3 | payload?: any; 4 | } 5 | -------------------------------------------------------------------------------- /app/actions/todos/todos.test.ts: -------------------------------------------------------------------------------- 1 | import { addTodo, removeTodo, updateTodo } from "./todos"; 2 | import { TodoData, Updates } from "./types"; 3 | 4 | describe("Todo action generators", () => { 5 | it("Generate add todo action", () => { 6 | const todo: TodoData = { 7 | type: "work", 8 | description: "YOSI BEZALHEL", 9 | isCompleted: true, 10 | }; 11 | 12 | const action = addTodo(todo); 13 | expect(action).toEqual({ 14 | type: "ADD_TODO", 15 | payload: { 16 | todo, 17 | }, 18 | }); 19 | }); 20 | 21 | it("Generate remove todo action", () => { 22 | const id = "k23brk324kb23"; 23 | const action = removeTodo(id); 24 | expect(action).toEqual({ 25 | type: "REMOVE_TODO", 26 | payload: { 27 | id, 28 | }, 29 | }); 30 | }); 31 | 32 | it("Generate update todo action", () => { 33 | const id = "blablabla"; 34 | const updates: Updates = { 35 | description: "lili", 36 | isCompleted: true, 37 | }; 38 | const action = updateTodo(id, updates); 39 | expect(action).toEqual({ 40 | type: "UPDATE_TODO", 41 | payload: { 42 | id, 43 | updates, 44 | }, 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /app/actions/todos/todos.ts: -------------------------------------------------------------------------------- 1 | import { Updates, TodoData, Action } from "./types"; 2 | import { Todo } from "../../types"; 3 | import * as todosApi from "../../services/todos-api"; 4 | 5 | const setTodos = (todos: Array): Action => ({ 6 | type: "SET_TODOS", 7 | payload: { todos }, 8 | }); 9 | 10 | export const startSetTodos = () => { 11 | return async (dispatch: any, getState: any) => { 12 | const { uid } = getState().auth.user; 13 | const todos = await todosApi.read(uid); 14 | dispatch(setTodos(todos)); 15 | }; 16 | }; 17 | 18 | const addTodo = (todo: Todo): Action => ({ 19 | type: "ADD_TODO", 20 | payload: { todo }, 21 | }); 22 | 23 | export const startAddTodo = (todoData: TodoData) => { 24 | return async (dispatch: any, getState: any) => { 25 | const { uid } = getState().auth.user; 26 | const id = await todosApi.create(uid, todoData); 27 | dispatch(addTodo({ id, ...todoData })); 28 | }; 29 | }; 30 | 31 | export const removeTodo = (id: string): Action => ({ 32 | type: "REMOVE_TODO", 33 | payload: { id }, 34 | }); 35 | 36 | export const startRemoveTodo = (id: string) => { 37 | return async (dispatch: any, getState: any) => { 38 | const { uid } = getState().auth.user; 39 | dispatch(removeTodo(id)); 40 | await todosApi.remove(uid, id); 41 | }; 42 | }; 43 | 44 | export const updateTodo = (id: string, updates: Updates): Action => ({ 45 | type: "UPDATE_TODO", 46 | payload: { 47 | id, 48 | updates, 49 | }, 50 | }); 51 | 52 | export const startUpdateTodo = (id: string, updates: Updates) => { 53 | return async (dispatch: any, getState: any) => { 54 | const { uid } = getState().auth.user; 55 | dispatch(updateTodo(id, updates)); 56 | await todosApi.update(uid, id, updates); 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /app/actions/todos/types.ts: -------------------------------------------------------------------------------- 1 | export interface TodoData { 2 | description: string; 3 | isCompleted: boolean; 4 | type: "personal" | "work"; 5 | } 6 | 7 | export interface Updates { 8 | description?: string; 9 | isCompleted?: boolean; 10 | type?: "personal" | "work"; 11 | } 12 | 13 | export interface Action { 14 | type: "ADD_TODO" | "REMOVE_TODO" | "UPDATE_TODO" | "SET_TODOS"; 15 | payload?: any; 16 | } 17 | -------------------------------------------------------------------------------- /app/app.tsx: -------------------------------------------------------------------------------- 1 | import "./utils/ignore-warnings"; 2 | import React from "react"; 3 | import { 4 | SafeAreaProvider, 5 | initialWindowMetrics, 6 | SafeAreaView, 7 | } from "react-native-safe-area-context"; 8 | import { useFonts } from "expo-font"; 9 | import { RootNavigator } from "./navigators"; 10 | import { Provider } from "react-redux"; 11 | import { PersistGate } from "redux-persist/integration/react"; 12 | import configureStore from "./store/configureStore"; 13 | import fonts from "./theme/fonts"; 14 | import ReactiveThemeProvider from "../app/components/reactive-theme-provider/reactive-theme-provider"; 15 | import { darkTheme, lightTheme } from "../app/theme/themes"; 16 | import styled from "@emotion/native"; 17 | 18 | const { store, persistor } = configureStore(); 19 | 20 | const StyledSafeAreaView = styled(SafeAreaView)(props => ({ 21 | flex: 1, 22 | backgroundColor: props.theme.background[100], 23 | })); 24 | 25 | function App() { 26 | const [loaded] = useFonts(fonts); 27 | 28 | if (!loaded) return null; 29 | 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default App; 46 | -------------------------------------------------------------------------------- /app/components/button/button.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle, TouchableOpacityProps } from "react-native"; 2 | 3 | export interface ButtonProps extends TouchableOpacityProps { 4 | /** 5 | * kinds of button, defaults to "primary" 6 | */ 7 | kind?: "primary" | "secondary" | "tertiary"; 8 | /** 9 | * the button text font size, defaults to 14s 10 | */ 11 | fontSize?: number; 12 | /** 13 | * Container style overrides 14 | */ 15 | style?: ViewStyle; 16 | } 17 | -------------------------------------------------------------------------------- /app/components/button/button.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { 4 | StoryScreen, 5 | Story, 6 | UseCase, 7 | Docs, 8 | PropsData, 9 | } from "../../../storybook/views"; 10 | import Button from "./button"; 11 | 12 | declare let module: any; 13 | 14 | const description = ` 15 | This is a button component that allows diffrent kinds of buttons. 16 | The button kinds are \`primary\`, \`secondary\`, \`tertiary\`. 17 | `; 18 | 19 | const propsData: PropsData = [ 20 | ["kind", "The button kind `primary | secondary | tertiary`", "primary"], 21 | ["fontSize", "the button text font size `number`", "14"], 22 | ["style", "Container style overrides `ViewStyle`", "-"], 23 | ]; 24 | 25 | storiesOf("Button", module) 26 | .addDecorator(fn => {fn()}) 27 | .add("📖 Docs", () => ( 28 | 29 | )) 30 | .add("Behaviour", () => ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 50 | 51 | 52 | 55 | 56 | 57 | )); 58 | -------------------------------------------------------------------------------- /app/components/button/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ButtonProps } from "./button.props"; 3 | import styled from "@emotion/native"; 4 | import { spacing, typography } from "../../theme"; 5 | 6 | interface ContainerProps { 7 | disabled: boolean; 8 | } 9 | const Container = styled.View(props => ({ 10 | opacity: props.disabled ? 0.3 : 1, 11 | })); 12 | 13 | interface TouchableOpacityProps { 14 | kind: "primary" | "secondary" | "tertiary"; 15 | } 16 | const TouchableOpacity = styled.TouchableOpacity( 17 | props => ({ 18 | backgroundColor: 19 | props.kind === "primary" 20 | ? props.theme.text[100] 21 | : props.theme.background[100], 22 | borderColor: props.theme.text[100], 23 | borderWidth: props.kind === "tertiary" ? 0 : 3, 24 | padding: spacing[3], 25 | flexDirection: props.kind === "tertiary" ? "row" : "column", 26 | alignSelf: props.kind === "tertiary" ? "flex-start" : "stretch", 27 | }), 28 | ); 29 | 30 | interface TextProps { 31 | kind: "primary" | "secondary" | "tertiary"; 32 | fontSize: number; 33 | } 34 | const Text = styled.Text(props => ({ 35 | color: 36 | props.kind === "primary" 37 | ? props.theme.background[100] 38 | : props.theme.text[100], 39 | fontFamily: typography.primary.bold, 40 | textAlign: "center", 41 | fontSize: props.fontSize, 42 | })); 43 | 44 | const Button: React.FC = props => { 45 | const { disabled, kind = "primary", fontSize = 14, ...buttonProps } = props; 46 | return ( 47 | 48 | 49 | 50 | {props.children} 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default Button; 58 | -------------------------------------------------------------------------------- /app/components/category-button/category-button.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | import { Category } from "../../types/filters"; 3 | 4 | export interface CategoryButtonProps { 5 | /** 6 | * Handle click of the button 7 | */ 8 | handleClick: () => void; 9 | /** 10 | * numbers of todos by category 11 | */ 12 | todosNumber: number; 13 | /** 14 | * numbers of todos that are completed by category 15 | */ 16 | todosCompleted: number; 17 | /** 18 | * The category type 19 | */ 20 | category: Category; 21 | /** 22 | * text 23 | */ 24 | text: string; 25 | /** 26 | * Container style overrides 27 | */ 28 | style?: ViewStyle; 29 | } 30 | -------------------------------------------------------------------------------- /app/components/category-button/category-button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { CategoryButtonProps } from "./category-button.props"; 3 | import { Pressable } from "react-native"; 4 | import { spacing } from "../../theme"; 5 | import styled from "@emotion/native"; 6 | import { typography } from "../../theme/typography"; 7 | import { Category } from "../../types/filters"; 8 | 9 | interface ContainerProps { 10 | pressed: boolean; 11 | category: Category; 12 | } 13 | const Container = styled.View(props => { 14 | const backgroundColor = { 15 | all: props.theme.text[100], 16 | personal: props.theme.primary[100], 17 | work: props.theme.primary[400], 18 | }; 19 | 20 | return { 21 | justifyContent: "flex-start", 22 | alignItems: "flex-start", 23 | backgroundColor: backgroundColor[props.category], 24 | paddingVertical: spacing[2], 25 | paddingHorizontal: spacing[2], 26 | marginVertical: spacing[2], 27 | marginRight: spacing[3], 28 | width: 120, 29 | height: 90, 30 | opacity: props.pressed ? 0.2 : 1, 31 | }; 32 | }); 33 | 34 | const Text = styled.Text(props => ({ 35 | color: props.theme.background[100], 36 | fontSize: 22, 37 | fontFamily: typography.primary.bold, 38 | })); 39 | 40 | const ProgressText = styled(Text)(() => ({ 41 | fontSize: 20, 42 | fontFamily: typography.primary.regular, 43 | })); 44 | 45 | const CategoryButton: React.FC = props => { 46 | const { style, handleClick, todosNumber, todosCompleted, text } = props; 47 | 48 | return ( 49 | 50 | {({ pressed }) => ( 51 | 52 | {text} 53 | {`${todosCompleted}/${todosNumber}`} 54 | 55 | )} 56 | 57 | ); 58 | }; 59 | 60 | export default CategoryButton; 61 | -------------------------------------------------------------------------------- /app/components/checkbox/checkbox.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | 3 | export interface CheckboxProps { 4 | /** 5 | * The height and width of the checkbox (height == width) 6 | */ 7 | size: number; 8 | /** 9 | * Is the checkbox checked 10 | */ 11 | isChecked: boolean; 12 | /** 13 | * Color for the unchecked checkbox 14 | */ 15 | color: string; 16 | /** 17 | * A callback function to run when checkbox is pressed 18 | */ 19 | handlePress: () => void; 20 | /** 21 | * Container style overrides 22 | */ 23 | style?: ViewStyle; 24 | } 25 | -------------------------------------------------------------------------------- /app/components/checkbox/checkbox.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { 4 | StoryScreen, 5 | Story, 6 | UseCase, 7 | Docs, 8 | PropsData, 9 | } from "../../../storybook/views"; 10 | import Checkbox from "./checkbox"; 11 | import { Toggle } from "react-powerplug"; 12 | 13 | declare let module: any; 14 | 15 | const description = ` 16 | A simple checkbox icon. 17 | `; 18 | 19 | const propsData: PropsData = [ 20 | [ 21 | "size", 22 | "The height and width of the checkbox (height == width) `number`", 23 | "-", 24 | ], 25 | ["isChecked", "Is the checkbox checked `boolean`", "-"], 26 | ["color", "Color for the unchecked checkbox `string`", "-"], 27 | [ 28 | "handlePress", 29 | "A callback function to run when checkbox is pressed `() => void`", 30 | "-", 31 | ], 32 | ["style", "Container style overrides `ViewStyle`", "-"], 33 | ]; 34 | 35 | storiesOf("Checkbox", module) 36 | .addDecorator(fn => {fn()}) 37 | .add("📖 Docs", () => ( 38 | 39 | )) 40 | .add("Behaviour", () => ( 41 | 42 | 43 | 44 | {({ on, toggle }) => ( 45 | 51 | )} 52 | 53 | 54 | 55 | 56 | {({ on, toggle }) => ( 57 | 63 | )} 64 | 65 | 66 | 67 | )); 68 | -------------------------------------------------------------------------------- /app/components/checkbox/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { CheckboxProps } from "./checkbox.props"; 3 | import { TouchableOpacity } from "react-native"; 4 | import Svg, { Circle, Path } from "react-native-svg"; 5 | import { useTheme } from "@emotion/react"; 6 | 7 | const Checkbox: React.FC = props => { 8 | const theme = useTheme(); 9 | 10 | return ( 11 | 12 | 18 | 28 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default Checkbox; 38 | -------------------------------------------------------------------------------- /app/components/error-message/error-message.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | 3 | export interface ErrorMessageProps { 4 | /** 5 | * The error message 6 | */ 7 | children: string; 8 | /** 9 | * Container style overrides 10 | */ 11 | style?: ViewStyle; 12 | } 13 | -------------------------------------------------------------------------------- /app/components/error-message/error-message.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { 4 | StoryScreen, 5 | Story, 6 | UseCase, 7 | Docs, 8 | PropsData, 9 | } from "../../../storybook/views"; 10 | import ErrorMessage from "./error-message"; 11 | 12 | declare let module: any; 13 | 14 | const description = ` 15 | This component displays an error message. 16 | `; 17 | 18 | const propsData: PropsData = [ 19 | ["children", "The error message `string`", "''"], 20 | ["style", "Container style overrides `ViewStyle`", "-"], 21 | ]; 22 | 23 | storiesOf("ErrorMessage", module) 24 | .addDecorator(fn => {fn()}) 25 | .add("📖 Docs", () => ( 26 | 31 | )) 32 | .add("Behaviour", () => ( 33 | 34 | 35 | Some Error 3453453 36 | 37 | 38 | )); 39 | -------------------------------------------------------------------------------- /app/components/error-message/error-message.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ErrorMessageProps } from "./error-message.props"; 3 | import styled from "@emotion/native"; 4 | import { spacing, typography } from "../../theme"; 5 | 6 | const Container = styled.View(() => ({ 7 | flexDirection: "row", 8 | paddingVertical: spacing[3], 9 | })); 10 | 11 | const Text = styled.Text(props => ({ 12 | fontFamily: typography.primary.bold, 13 | fontSize: 15, 14 | color: props.theme.error[100], 15 | fontWeight: "600", 16 | letterSpacing: 1.5, 17 | })); 18 | 19 | const ErrorMessage: React.FC = props => { 20 | return ( 21 | 22 | {props.children} 23 | 24 | ); 25 | }; 26 | 27 | export default ErrorMessage; 28 | -------------------------------------------------------------------------------- /app/components/filters-form/filters-form.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | import { Category, Sort } from "../../types/filters"; 3 | 4 | export interface FiltersFormProps { 5 | /** 6 | * filtres todos 7 | */ 8 | updateFilterBy: (type: Category) => void; 9 | /** 10 | * sort todos 11 | */ 12 | updateSortBy: (type: Sort) => void; 13 | /** 14 | * contains the counters of every todo by category 15 | */ 16 | todosCount: { 17 | [type in Category]: { 18 | total: number; 19 | completed: number; 20 | }; 21 | }; 22 | /** 23 | * Container style overrides 24 | */ 25 | style?: ViewStyle; 26 | } 27 | -------------------------------------------------------------------------------- /app/components/filters-form/filters-form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { FiltersFormProps } from "./filters-form.props"; 3 | import { View, ViewStyle } from "react-native"; 4 | import { spacing } from "../../theme"; 5 | import { Category, Sort } from "../../types/filters"; 6 | import CategoryButton from "../category-button/category-button"; 7 | import { connect } from "react-redux"; 8 | import { updateFilterBy, updateSortBy } from "../../actions/filters/filters"; 9 | import { calculateTodosCount } from "../../selectors/todos"; 10 | import { translate } from "../../i18n"; 11 | import styled from "@emotion/native"; 12 | import Button from "../button/button"; 13 | 14 | const TODO_TYPES_CONTAINER: ViewStyle = { 15 | paddingHorizontal: spacing[6], 16 | }; 17 | 18 | const TodoTypes = styled.FlatList(() => ({ 19 | paddingVertical: spacing[3], 20 | })); 21 | 22 | const SortOptions = styled.View(() => ({ 23 | flexDirection: "row", 24 | paddingVertical: spacing[1], 25 | marginLeft: spacing[5], 26 | })); 27 | 28 | const SortButton = styled(Button)(() => ({})); 29 | 30 | const FiltersForm: React.FC = props => { 31 | return ( 32 | 33 | { 36 | const category: Category = data.item; 37 | return ( 38 | props.updateFilterBy(category)} 44 | category={category} 45 | /> 46 | ); 47 | }} 48 | keyExtractor={(category: Category) => category} 49 | horizontal={true} 50 | contentContainerStyle={TODO_TYPES_CONTAINER} 51 | /> 52 | 53 | props.updateSortBy("nameAsc")} 56 | > 57 | {translate("sortBy.asc")} 58 | 59 | props.updateSortBy("nameDes")} 62 | > 63 | {translate("sortBy.des")} 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | const mapStateToProps = (state: any) => ({ 71 | todosCount: calculateTodosCount(state.todos), 72 | }); 73 | 74 | const mapDispatchToProps = (dispatch: any) => ({ 75 | updateFilterBy: (type: Category) => dispatch(updateFilterBy(type)), 76 | updateSortBy: (type: Sort) => dispatch(updateSortBy(type)), 77 | }); 78 | 79 | export default connect(mapStateToProps, mapDispatchToProps)(FiltersForm); 80 | -------------------------------------------------------------------------------- /app/components/header/header.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | 3 | export interface HeaderProps { 4 | /** 5 | * handle click of the back button 6 | */ 7 | handleBackButtonClick: () => void; 8 | /** 9 | * Container style overrides 10 | */ 11 | style?: ViewStyle; 12 | } 13 | -------------------------------------------------------------------------------- /app/components/header/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HeaderProps } from "./header.props"; 3 | import { 4 | TextStyle, 5 | View, 6 | ViewStyle, 7 | Text, 8 | TouchableOpacity, 9 | } from "react-native"; 10 | import { spacing } from "../../theme"; 11 | 12 | const CONTAINER: ViewStyle = { 13 | paddingHorizontal: spacing[5], 14 | }; 15 | 16 | const BACK_BUTTION: TextStyle = { 17 | fontSize: spacing[6], 18 | }; 19 | 20 | const Header: React.FC = props => { 21 | const { style, handleBackButtonClick } = props; 22 | 23 | return ( 24 | 25 | handleBackButtonClick()}> 26 | {"👈🏻"} 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default Header; 33 | -------------------------------------------------------------------------------- /app/components/heading/heading.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | 3 | export interface HeadingProps { 4 | /** 5 | * scaling of the header (1 is largest) 6 | * defaults to 1 7 | */ 8 | scale?: 1 | 2 | 3; 9 | /** 10 | * Text for heading 11 | */ 12 | children: string; 13 | /** 14 | * Container style overrides 15 | */ 16 | style?: ViewStyle; 17 | } 18 | -------------------------------------------------------------------------------- /app/components/heading/heading.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { 4 | StoryScreen, 5 | Story, 6 | UseCase, 7 | Docs, 8 | PropsData, 9 | } from "../../../storybook/views"; 10 | import Heading from "./heading"; 11 | 12 | declare let module: any; 13 | 14 | const description = ` 15 | This is a heading component that allows scaling. 16 | The heading scales from 1 to 3, to match the HTML tags ${`h1`}, ${`h2`}, ${`h3`}. 17 | `; 18 | 19 | const propsData: PropsData = [ 20 | ["children", "The heading text `string`", "-"], 21 | ["scale", "The heading scale `1 | 2 | 3`", "1"], 22 | ["style", "Container style overrides `ViewStyle`", "-"], 23 | ]; 24 | 25 | storiesOf("Heading", module) 26 | .addDecorator(fn => {fn()}) 27 | .add("📖 Docs", () => ( 28 | 29 | )) 30 | .add("Behaviour", () => ( 31 | 32 | 33 | Heading Scale 1 34 | Heading Scale 2 35 | Heading Scale 3 36 | 37 | 38 | )); 39 | -------------------------------------------------------------------------------- /app/components/heading/heading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { HeadingProps } from "./Heading.props"; 3 | import { spacing, typography } from "../../theme"; 4 | import styled from "@emotion/native"; 5 | 6 | const Container = styled.View(() => ({ 7 | flexDirection: "row", 8 | borderColor: "red", 9 | })); 10 | 11 | interface TextProps { 12 | fontSize: number; 13 | } 14 | const Text = styled.Text(props => ({ 15 | fontFamily: typography.primary.bold, 16 | fontSize: props.fontSize, 17 | color: props.theme.text[100], 18 | fontWeight: "bold", 19 | })); 20 | 21 | const Heading: React.FC = ({ children, scale = 1, style }) => { 22 | const fontSize = { 23 | 1: spacing[6], 24 | 2: spacing[5], 25 | 3: spacing[4], 26 | }; 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | }; 34 | 35 | export default Heading; 36 | -------------------------------------------------------------------------------- /app/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./todo-item/todo-item"; 2 | export * from "./checkbox/checkbox"; 3 | export * from "./heading/heading"; 4 | export * from "./todo-list/todo-list"; 5 | export * from "./new-todo-form/new-todo-form"; 6 | export * from "./category-button/category-button"; 7 | export * from "./filters-form/filters-form"; 8 | export * from "./header/header"; 9 | export * from "./screen/screen"; 10 | export * from "./button/button"; 11 | export * from "./error-message/error-message"; 12 | export * from "./loading-button/loading-button"; 13 | export * from "./reactive-theme-provider/reactive-theme-provider"; 14 | export * from "./input/input"; 15 | export * from "./button/button"; 16 | export * from "./todo-item/todo-item"; 17 | export * from "./checkbox/checkbox"; 18 | -------------------------------------------------------------------------------- /app/components/input/input.props.ts: -------------------------------------------------------------------------------- 1 | import { TextInputProps, ViewStyle } from "react-native"; 2 | 3 | export interface InputProps extends TextInputProps { 4 | /** 5 | * is input valid 6 | */ 7 | error?: boolean; 8 | /** 9 | * error message to display when error is set to true 10 | */ 11 | errorMessage?: string; 12 | /** 13 | * label to be placed on top of the input 14 | */ 15 | label?: string; 16 | /** 17 | * Container style overrides 18 | */ 19 | style?: ViewStyle; 20 | } 21 | -------------------------------------------------------------------------------- /app/components/input/input.story.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { 4 | StoryScreen, 5 | Story, 6 | UseCase, 7 | Docs, 8 | PropsData, 9 | } from "../../../storybook/views"; 10 | import Input from "./input"; 11 | 12 | declare let module: any; 13 | 14 | const description = ` 15 | This is a wrapper around React Native \`TextInput\` component. It can display a label if given one, and an error message below the input if the error argument is set to true. 16 | 17 | \`Input\` inherits the \`TextInput\` props. 18 | `; 19 | 20 | const propsData: PropsData = [ 21 | ["error", "Should an error message be displayed `boolean`", "false"], 22 | [ 23 | "errorMessage", 24 | "error message to display when error is set to true `string`", 25 | "''", 26 | ], 27 | ["label", "label to be placed on top of the input `string`", "''"], 28 | ["style", "Container style overrides `ViewStyle`", "-"], 29 | ]; 30 | 31 | storiesOf("Input", module) 32 | .addDecorator(fn => {fn()}) 33 | .add("📖 Docs", () => ( 34 | 35 | )) 36 | .add("Behaviour", () => ( 37 | 38 | 39 | 40 | 41 | 42 | 46 | 47 | 48 | 49 | 53 | 59 | 60 | 61 | )); 62 | -------------------------------------------------------------------------------- /app/components/input/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { InputProps } from "./input.props"; 3 | import styled from "@emotion/native"; 4 | import { spacing, typography } from "../../theme"; 5 | import Heading from "../heading/heading"; 6 | import ErrorMessage from "../error-message/error-message"; 7 | 8 | const Container = styled.View(() => ({})); 9 | 10 | const Label = styled(Heading)(() => ({ 11 | paddingVertical: spacing[3], 12 | })); 13 | 14 | interface TextInputProps { 15 | error: boolean; 16 | } 17 | const TextInput = styled.TextInput(props => ({ 18 | flexDirection: "row", 19 | padding: spacing[4], 20 | backgroundColor: props.theme.background[100], 21 | borderColor: props.error ? props.theme.error[100] : props.theme.text[100], 22 | borderWidth: 3, 23 | fontSize: spacing[5], 24 | color: props.theme.text[100], 25 | fontFamily: typography.primary.regular, 26 | })); 27 | 28 | const Input: React.FC = props => { 29 | const { style, label, error, errorMessage, ...textInputProps } = props; 30 | 31 | return ( 32 | 33 | {label && } 34 | 35 | {error && {errorMessage}} 36 | 37 | ); 38 | }; 39 | 40 | export default Input; 41 | -------------------------------------------------------------------------------- /app/components/loading-button/loading-button.props.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native"; 2 | import { ButtonProps } from "../button/button.props"; 3 | 4 | export interface LoadingButtonProps extends ButtonProps { 5 | /** 6 | * is button in loading state 7 | */ 8 | isLoading: boolean; 9 | /** 10 | * error message or undefined if no error exist 11 | */ 12 | error: string | null; 13 | /** 14 | * Container style overrides 15 | */ 16 | style?: ViewStyle; 17 | } 18 | -------------------------------------------------------------------------------- /app/components/loading-button/loading-button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { ActivityIndicator, View, ViewStyle } from "react-native"; 3 | import { LoadingButtonProps } from "./loading-button.props"; 4 | import Button from "../button/button"; 5 | import ErrorMessage from "../error-message/error-message"; 6 | 7 | const CONTAINER: ViewStyle = {}; 8 | 9 | const LoadingButton: React.FC = props => { 10 | const { style, error, isLoading, ...buttonProps } = props; 11 | 12 | if (isLoading) 13 | return ( 14 | 15 | 16 | 17 | ); 18 | 19 | return ( 20 | 21 |