├── .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 |

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 |
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 |
22 | {error && {error}}
23 |
24 | );
25 | };
26 |
27 | export default LoadingButton;
28 |
--------------------------------------------------------------------------------
/app/components/new-todo-form/new-todo-form.props.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from "react-native";
2 | import { TodoData } from "../../actions/todos/types";
3 |
4 | export interface NewTodoFormProps {
5 | /**
6 | * adds todo
7 | */
8 | addTodo: (todo: TodoData) => void;
9 | /**
10 | * Container style overrides
11 | */
12 | style?: ViewStyle;
13 | }
14 |
--------------------------------------------------------------------------------
/app/components/new-todo-form/new-todo-form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { NewTodoFormProps } from "./new-todo-form.props";
3 | import { useState } from "react";
4 | import { spacing } from "../../theme";
5 | import { connect } from "react-redux";
6 | import Heading from "../heading/heading";
7 | import { startAddTodo } from "../../actions/todos/todos";
8 | import { TodoData } from "../../actions/todos/types";
9 | import { translate } from "../../i18n";
10 | import styled from "@emotion/native";
11 | import Input from "../input/input";
12 | import Button from "../button/button";
13 | import { Category } from "../../types/filters";
14 | import { Keyboard } from "react-native";
15 |
16 | const Container = styled.View(() => ({
17 | paddingHorizontal: spacing[5],
18 | paddingVertical: spacing[2],
19 | }));
20 |
21 | const SubHeading = styled(Heading)(() => ({
22 | paddingBottom: spacing[2],
23 | }));
24 |
25 | const Form = styled.View(() => ({
26 | flexDirection: "row",
27 | }));
28 |
29 | const InputContainer = styled.View(() => ({
30 | flex: 1,
31 | paddingRight: spacing[1],
32 | }));
33 |
34 | const TextInput = styled(Input)(() => ({
35 | fontSize: spacing[5],
36 | paddingVertical: spacing[2],
37 | }));
38 |
39 | interface SubmitButtonProps {
40 | category: Category;
41 | }
42 | const SubmitButton = styled(Button)(props => ({
43 | marginHorizontal: spacing[1],
44 | flex: 1,
45 | backgroundColor:
46 | props.category === "personal"
47 | ? props.theme.primary[100]
48 | : props.theme.primary[400],
49 | borderColor:
50 | props.category === "personal"
51 | ? props.theme.primary[100]
52 | : props.theme.primary[400],
53 | }));
54 |
55 | const NewTodoForm: React.FC = props => {
56 | const [description, setDescription] = useState("");
57 |
58 | function handleSubmitTodo(type: "work" | "personal") {
59 | props.addTodo({
60 | isCompleted: false,
61 | description,
62 | type,
63 | });
64 | setDescription("");
65 | Keyboard.dismiss();
66 | }
67 |
68 | return (
69 |
70 |
71 | {translate("homeScreen.subtitle.newTask")}
72 |
73 |
92 |
93 | );
94 | };
95 |
96 | const mapDispatchToProps = (dispatch: any) => ({
97 | addTodo: (todo: TodoData) => dispatch(startAddTodo(todo)),
98 | });
99 |
100 | export default connect(undefined, mapDispatchToProps)(NewTodoForm);
101 |
--------------------------------------------------------------------------------
/app/components/reactive-theme-provider/reactive-theme-provider.props.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { ViewStyle } from "react-native";
3 | import { ThemeInterface } from "../../types/theme";
4 |
5 | export interface ReactiveThemeProviderProps {
6 | /**
7 | * The entire application
8 | */
9 | children: ReactNode;
10 | /**
11 | * The light theme interface
12 | */
13 | lightTheme: ThemeInterface;
14 | /**
15 | * The dark theme interface
16 | */
17 | darkTheme: ThemeInterface;
18 | /**
19 | * Container style overrides
20 | */
21 | style?: ViewStyle;
22 | }
23 |
--------------------------------------------------------------------------------
/app/components/reactive-theme-provider/reactive-theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ReactiveThemeProviderProps } from "./reactive-theme-provider.props";
3 | import { ThemeProvider } from "@emotion/react";
4 | import { useReactiveTheme } from "../../hooks";
5 | import { ThemeInterface } from "../../types/theme";
6 |
7 | const ReactiveThemeProvider: React.FC = ({
8 | lightTheme,
9 | darkTheme,
10 | children,
11 | }) => {
12 | const theme = useReactiveTheme(lightTheme, darkTheme);
13 |
14 | return {children};
15 | };
16 |
17 | export default ReactiveThemeProvider;
18 |
--------------------------------------------------------------------------------
/app/components/screen/screen.props.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from "react-native";
2 |
3 | export interface ScreenProps {
4 | /**
5 | * should render a header
6 | */
7 | showHeader?: boolean;
8 | /**
9 | * Container style overrides
10 | */
11 | style?: ViewStyle;
12 | }
13 |
--------------------------------------------------------------------------------
/app/components/screen/screen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ScreenProps } from "./screen.props";
3 | import { useNavigation } from "@react-navigation/native";
4 | import { useIsFirstRoute } from "../../hooks";
5 | import Header from "../header/header";
6 | import styled from "@emotion/native";
7 |
8 | const Container = styled.View(props => ({
9 | flex: 1,
10 | backgroundColor: props.theme.background[100],
11 | }));
12 |
13 | const Screen: React.FC = props => {
14 | const { style, showHeader = true } = props;
15 | const navigation = useNavigation();
16 | const isFirstRoute = useIsFirstRoute();
17 |
18 | return (
19 |
20 | {!isFirstRoute && showHeader && (
21 | navigation.goBack()} />
22 | )}
23 | {props.children}
24 |
25 | );
26 | };
27 |
28 | export default Screen;
29 |
--------------------------------------------------------------------------------
/app/components/todo-item/todo-item.props.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from "react-native";
2 |
3 | export interface TodoProps {
4 | /**
5 | * Toggle the isCompleted property of the todo with the id provided
6 | */
7 | handleClickCheckbox: (id: string, isCompleted: boolean) => void;
8 | /**
9 | * remove an item
10 | */
11 | handleClickRemove: (id: string) => void;
12 | /**
13 | * The type of the todo
14 | */
15 | type: "personal" | "work";
16 | /**
17 | * The todo description
18 | */
19 | description: string;
20 | /**
21 | * Is the todo completed
22 | */
23 | isCompleted: boolean;
24 | /**
25 | * The id of the todo
26 | */
27 | id: string;
28 | /**
29 | * Container style overrides
30 | */
31 | style?: ViewStyle;
32 | }
33 |
--------------------------------------------------------------------------------
/app/components/todo-item/todo-item.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 { TodoItem } from "./todo-item";
11 | import { Toggle } from "react-powerplug";
12 |
13 | declare let module: any;
14 |
15 | const description = `
16 | Displays a todo item, with option to toggle completion and to delete the item.
17 | `;
18 |
19 | const propsData: PropsData = [
20 | [
21 | "handleClickCheckbox",
22 | "Toggle the isCompleted property of the todo with the id provided `(id: string, isCompleted: boolean) => void`",
23 | "-",
24 | ],
25 | ["handleClickRemove", "remove an item `(id: string) => void`", "-"],
26 | ["type", "The type of the todo `'personal' | 'work'`", "-"],
27 | ["description", "The todo description `string`", "-"],
28 | ["isCompleted", "Is the todo completed `boolean`", "-"],
29 | ["id", "The id of the todo `string`", "-"],
30 | ["style", "Container style overrides `ViewStyle`", "-"],
31 | ];
32 |
33 | storiesOf("TodoItem", module)
34 | .addDecorator(fn => {fn()})
35 | .add("📖 Docs", () => (
36 |
37 | ))
38 | .add("Behaviour", () => (
39 |
40 |
41 | {["Pay rent", "Learn React", "Buy high sell low"].map(todo => (
42 |
43 | {({ on, toggle }) => (
44 | ({})}
47 | type="work"
48 | isCompleted={on}
49 | description={todo}
50 | id="id"
51 | />
52 | )}
53 |
54 | ))}
55 |
56 |
61 |
62 | {({ on, toggle }) => (
63 | ({})}
66 | type="work"
67 | isCompleted={on}
68 | description="Work type"
69 | id="id"
70 | />
71 | )}
72 |
73 |
74 | {({ on, toggle }) => (
75 | ({})}
78 | type="personal"
79 | isCompleted={on}
80 | description="Personal type"
81 | id="id"
82 | />
83 | )}
84 |
85 |
86 |
87 | ));
88 |
--------------------------------------------------------------------------------
/app/components/todo-item/todo-item.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { TodoProps } from "./todo-item.props";
3 | import { spacing, typography } from "../../theme";
4 | import styled from "@emotion/native";
5 | import { connect } from "react-redux";
6 | import Checkbox from "../checkbox/checkbox";
7 | import { startRemoveTodo, startUpdateTodo } from "../../actions/todos/todos";
8 | import { useTheme } from "@emotion/react";
9 | import Button from "../button/button";
10 | import { Category } from "../../types/filters";
11 |
12 | const Container = styled.View({
13 | flex: 1,
14 | flexDirection: "row",
15 | marginHorizontal: spacing[5],
16 | marginVertical: spacing[1],
17 | });
18 |
19 | interface TodoContainerProps {
20 | category: Category;
21 | }
22 | const TodoContainer = styled.View(props => ({
23 | flex: 1,
24 | flexDirection: "row",
25 | padding: spacing[5],
26 | backgroundColor:
27 | props.category === "personal"
28 | ? props.theme.primary[100]
29 | : props.theme.primary[400],
30 | }));
31 |
32 | const CheckboxContainer = styled.View(() => ({
33 | justifyContent: "center",
34 | alignItems: "center",
35 | }));
36 |
37 | const DescriptionContainer = styled.View(() => ({
38 | justifyContent: "center",
39 | marginLeft: spacing[5],
40 | }));
41 |
42 | interface DescriptionProps {
43 | isCompleted: boolean;
44 | }
45 | const Description = styled.Text(props => ({
46 | fontFamily: typography.primary.regular,
47 | fontSize: 30,
48 | color: props.theme.background[100],
49 | textDecorationLine: props.isCompleted ? "line-through" : "none",
50 | opacity: props.isCompleted ? 0.3 : 1,
51 | }));
52 |
53 | interface DeleteButtonProps {
54 | category: Category;
55 | }
56 | const DeleteButton = styled(Button)(props => ({
57 | padding: spacing[5],
58 | backgroundColor:
59 | props.category === "personal"
60 | ? props.theme.primary[100]
61 | : props.theme.primary[400],
62 | borderColor:
63 | props.category === "personal"
64 | ? props.theme.primary[100]
65 | : props.theme.primary[400],
66 | height: "100%",
67 | justifyContent: "center",
68 | }));
69 |
70 | export const TodoItem: React.FC = props => {
71 | const { style, description, isCompleted, type, id } = props;
72 | const theme = useTheme();
73 |
74 | return (
75 |
76 |
77 |
78 | props.handleClickCheckbox(id, isCompleted)}
85 | />
86 |
87 |
88 | {description}
89 |
90 |
91 | props.handleClickRemove(id)}
93 | category={type}
94 | kind="primary"
95 | fontSize={30}
96 | >
97 | 🗑
98 |
99 |
100 | );
101 | };
102 |
103 | const mapDispatchToProps = (dispatch: any) => ({
104 | handleClickCheckbox: (id: string, isCompleted: boolean) => {
105 | dispatch(startUpdateTodo(id, { isCompleted: !isCompleted }));
106 | },
107 | handleClickRemove: (id: string) => {
108 | dispatch(startRemoveTodo(id));
109 | },
110 | });
111 |
112 | export default connect(undefined, mapDispatchToProps)(TodoItem);
113 |
--------------------------------------------------------------------------------
/app/components/todo-list/todo-list.props.ts:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from "react-native";
2 | import { Todo } from "../../types";
3 |
4 | export interface TodoListProps {
5 | /**
6 | * fetches and set the todos
7 | */
8 | setTodos: () => Promise;
9 | /**
10 | * array of todo objects
11 | */
12 | todos: Array;
13 | /**
14 | * Container style overrides
15 | */
16 | style?: ViewStyle;
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/todo-list/todo-list.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { connect } from "react-redux";
3 | import { TodoListProps } from "./todo-list.props";
4 | import { ViewStyle, ActivityIndicator } from "react-native";
5 | import styled from "@emotion/native";
6 | import { Todo } from "../../types";
7 | import { filterTodos } from "../../selectors/todos";
8 | import { startSetTodos } from "../../actions/todos/todos";
9 | import { spacing } from "../../theme";
10 | import { translate } from "../../i18n";
11 | import Heading from "../heading/heading";
12 | import TodoItem from "../todo-item/todo-item";
13 |
14 | const Container = styled.View(() => ({
15 | flex: 1,
16 | marginTop: spacing[3],
17 | }));
18 |
19 | const SubHeading = styled(Heading)(() => ({
20 | paddingHorizontal: spacing[5],
21 | marginBottom: spacing[3],
22 | }));
23 |
24 | const Todos = styled.FlatList(() => ({
25 | flex: 1,
26 | }));
27 |
28 | const TODOS_CONTAINER: ViewStyle = {
29 | paddingBottom: spacing[5],
30 | };
31 |
32 | export const TodoList: React.FC = props => {
33 | const { style } = props;
34 |
35 | const [isLoading, setIsLoading] = React.useState(true);
36 |
37 | React.useEffect(() => {
38 | props.setTodos().then(() => setIsLoading(false));
39 | }, []);
40 |
41 | if (isLoading) return ;
42 |
43 | return (
44 |
45 |
46 | {translate("homeScreen.subtitle.todayTasks")}
47 |
48 | }
51 | keyExtractor={(todo: Todo): string => todo.id}
52 | contentContainerStyle={TODOS_CONTAINER}
53 | />
54 |
55 | );
56 | };
57 |
58 | const mapDispatchToProps = (dispatch: any) => ({
59 | setTodos: () => dispatch(startSetTodos()),
60 | });
61 |
62 | const mapStateToProps = (state: any) => ({
63 | todos: filterTodos(state.todos, state.filters),
64 | });
65 |
66 | export default connect(mapStateToProps, mapDispatchToProps)(TodoList);
67 |
--------------------------------------------------------------------------------
/app/firebase/firebase.ts:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | import "firebase/database";
3 | import "firebase/auth";
4 |
5 | import {
6 | FIREBASE_API_KEY,
7 | FIREBASE_AUTH_DOMAIN,
8 | FIREBASE_DATABASE_URL,
9 | FIREBASE_PROJECT_ID,
10 | FIREBASE_STORAGE_BUCKET,
11 | FIREBASE_MESSAGING_SENDER_ID,
12 | FIREBASE_APP_ID,
13 | FIREBASE_MEASUREMENT_ID,
14 | } from "@env";
15 |
16 | const config = {
17 | apiKey: FIREBASE_API_KEY,
18 | authDomain: FIREBASE_AUTH_DOMAIN,
19 | databaseURL: FIREBASE_DATABASE_URL,
20 | projectId: FIREBASE_PROJECT_ID,
21 | storageBucket: FIREBASE_STORAGE_BUCKET,
22 | messagingSenderId: FIREBASE_MESSAGING_SENDER_ID,
23 | appId: FIREBASE_APP_ID,
24 | measurementId: FIREBASE_MEASUREMENT_ID,
25 | };
26 |
27 | if (!firebase.apps.length) {
28 | firebase.initializeApp(config);
29 | }
30 |
31 | const database = firebase.database();
32 | const auth = firebase.auth();
33 |
34 | export { firebase, database, auth };
35 |
--------------------------------------------------------------------------------
/app/firebase/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./firebase";
2 |
--------------------------------------------------------------------------------
/app/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useIsFirstRoute";
2 | export * from "./useCredentialsFields";
3 | export * from "./useReactiveTheme";
4 |
--------------------------------------------------------------------------------
/app/hooks/useCredentialsFields.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback } from "react";
2 | import { validateEmail, validatePassword } from "../utils/validate";
3 | import { debounce } from "debounce";
4 | import { timing } from "../theme";
5 |
6 | export function useCredentialsFields() {
7 | const [email, setEmail] = useState("");
8 | const [password, setPassword] = useState("");
9 |
10 | const [emailIsValid, setEmailIsValid] = useState(true);
11 | const [passwordIsValid, setPasswordIsValid] = useState(true);
12 |
13 | useEffect(() => {
14 | updateEmailIsValid(email);
15 | }, [email]);
16 |
17 | const updateEmailIsValid = useCallback(
18 | debounce(
19 | (email: string) => setEmailIsValid(!email || validateEmail(email)),
20 | emailIsValid ? timing.long : timing.quick,
21 | ),
22 | [emailIsValid],
23 | );
24 |
25 | useEffect(() => {
26 | setPasswordIsValid(validatePassword(password));
27 | }, [password]);
28 |
29 | return {
30 | email: {
31 | value: email,
32 | update: setEmail,
33 | isValid: emailIsValid,
34 | },
35 | password: {
36 | value: password,
37 | update: setPassword,
38 | isValid: passwordIsValid,
39 | },
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/app/hooks/useIsFirstRoute.ts:
--------------------------------------------------------------------------------
1 | import { useNavigation, useRoute } from "@react-navigation/core";
2 |
3 | export function useIsFirstRoute() {
4 | const navigation = useNavigation();
5 | const route = useRoute();
6 |
7 | return navigation.dangerouslyGetState().routes[0].key === route.key;
8 | }
9 |
--------------------------------------------------------------------------------
/app/hooks/useReactiveTheme.ts:
--------------------------------------------------------------------------------
1 | import { useColorScheme } from "react-native";
2 | import { useState, useEffect } from "react";
3 |
4 | export function useReactiveTheme(lightTheme: T, darkTheme: T): T {
5 | const [theme, setTheme] = useState(lightTheme);
6 | const colorScheme = useColorScheme();
7 |
8 | useEffect(() => {
9 | setTheme(colorScheme === "light" ? lightTheme : darkTheme);
10 | }, [colorScheme]);
11 |
12 | return theme;
13 | }
14 |
--------------------------------------------------------------------------------
/app/i18n/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "ok": "OK!",
4 | "cancel": "Cancel",
5 | "back": "Back",
6 | "signIn": "Sign In",
7 | "signUp": "Sign Up",
8 | "email": "Email",
9 | "password": "Password",
10 | "placeholder": {
11 | "email": "Enter email",
12 | "password": "Enter password"
13 | }
14 | },
15 | "errors": {
16 | "invalidEmail": "Invalid email address.",
17 | "invalidPassword": "Password must be at least 6 character long, with one upper case letter and one number."
18 | },
19 | "welcomeScreen": {
20 | "title": "Welcome!",
21 | "signInMethods": {
22 | "email": "Email and Password",
23 | "anonymous": "Anonymous",
24 | "gmail": "Gmail",
25 | "facebook": "Facebook"
26 | }
27 | },
28 | "signInWithEmailScreen": {
29 | "title": "Sign in with email",
30 | "signUpMessage": "Don't have an account?"
31 | },
32 | "signUpWithEmailScreen": {
33 | "title": "Sign up with email"
34 | },
35 | "homeScreen": {
36 | "title": "Hello",
37 | "subtitle": {
38 | "newTask": "ADD NEW TASK",
39 | "todayTasks": "TODAY'S TASKS"
40 | }
41 | },
42 | "todos": {
43 | "categories": {
44 | "personal": "personal",
45 | "work": "work",
46 | "all": "all"
47 | }
48 | },
49 | "sortBy": {
50 | "asc": "Sort by A-Z",
51 | "des": "Sort by Z-A"
52 | }
53 | }
--------------------------------------------------------------------------------
/app/i18n/he.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "ok": "אוקיי!",
4 | "cancel": "ביטול",
5 | "back": "חזור",
6 | "signIn": "כנס",
7 | "signUp": "הירשם",
8 | "email": "אימייל",
9 | "password": "סיסמא",
10 | "placeholder": {
11 | "email": "הכנס מייל",
12 | "password": "הכנס סיסמא"
13 | }
14 | },
15 | "errors": {
16 | "invalidEmail": "כתובת מייל שגויה.",
17 | "invalidPassword": "הסיסמא חייבת להכיל לפחות 6 תווים, לכלול אות גדולה וספרה."
18 | },
19 | "welcomeScreen": {
20 | "title": "ברוך הבא!",
21 | "signInMethods": {
22 | "email": "אימייל",
23 | "anonymous": "אנונימי",
24 | "gmail": "גוגל",
25 | "facebook": "פייסבוק"
26 | }
27 | },
28 | "signInWithEmailScreen": {
29 | "title": "המשך עם אימייל",
30 | "signUpMessage": "אין לך חשבון?"
31 | },
32 | "signUpWithEmailScreen": {
33 | "title": "הירשם עם אימייל"
34 | },
35 | "homeScreen": {
36 | "title": "שלום",
37 | "subtitle": {
38 | "newTask": "הוסף משימה חדשה",
39 | "todayTasks": "המשימות שלך"
40 | }
41 | },
42 | "todos": {
43 | "categories": {
44 | "personal": "אישי",
45 | "work": "עבודה",
46 | "all": "הכול"
47 | }
48 | },
49 | "sortBy": {
50 | "asc": "מיין בסדר עולה",
51 | "des": "מיין בסדר יורד"
52 | }
53 | }
--------------------------------------------------------------------------------
/app/i18n/i18n.ts:
--------------------------------------------------------------------------------
1 | import * as Localization from "expo-localization";
2 | import i18n from "i18n-js";
3 | import en from "./en.json";
4 | import he from "./he.json";
5 |
6 | i18n.fallbacks = true;
7 | i18n.translations = { en, he };
8 |
9 | i18n.locale = Localization.locale || "en";
10 |
11 | type DefaultLocale = typeof en;
12 | export type TxKeyPath = RecursiveKeyOf;
13 |
14 | type RecursiveKeyOf> = {
15 | [TKey in keyof TObj & string]: TObj[TKey] extends Record
16 | ? `${TKey}` | `${TKey}.${RecursiveKeyOf}`
17 | : `${TKey}`;
18 | }[keyof TObj & string];
19 |
--------------------------------------------------------------------------------
/app/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import "./i18n"
2 | export * from "./i18n"
3 | export * from "./translate"
4 |
--------------------------------------------------------------------------------
/app/i18n/translate.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18n-js"
2 | import { TxKeyPath } from "./i18n"
3 |
4 | /**
5 | * Translates text.
6 | *
7 | * @param key The i18n key.
8 | */
9 | export function translate(key: TxKeyPath, options?: i18n.TranslateOptions) {
10 | return key ? i18n.t(key, options) : null
11 | }
12 |
--------------------------------------------------------------------------------
/app/navigators/auth-navigator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createStackNavigator } from "@react-navigation/stack";
3 | import {
4 | WelcomeScreen,
5 | SignInWithEmailScreen,
6 | SignUpWithEmailScreen,
7 | } from "../screens";
8 |
9 | export type PrimaryParamList = {
10 | Welcome: undefined;
11 | SignInWithEmail: undefined;
12 | SignUpWithEmail: undefined;
13 | };
14 |
15 | const Stack = createStackNavigator();
16 |
17 | export function AuthNavigator() {
18 | return (
19 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | /**
34 | * A list of routes from which we're allowed to leave the app when
35 | * the user presses the back button on Android.
36 | *
37 | * Anything not on this list will be a standard `back` action in
38 | * react-navigation.
39 | */
40 | const exitRoutes = ["signIn"];
41 | export const canExit = (routeName: string) => exitRoutes.includes(routeName);
42 |
--------------------------------------------------------------------------------
/app/navigators/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./main-navigator";
2 | export * from "./root-navigator";
3 | export * from "./navigation-utilities";
4 |
--------------------------------------------------------------------------------
/app/navigators/main-navigator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createStackNavigator } from "@react-navigation/stack";
3 | import { HomeScreen } from "../screens";
4 |
5 | export type PrimaryParamList = {
6 | home: undefined;
7 | };
8 |
9 | const Stack = createStackNavigator();
10 |
11 | export function MainNavigator() {
12 | return (
13 |
20 |
21 |
22 | );
23 | }
24 |
25 | /**
26 | * A list of routes from which we're allowed to leave the app when
27 | * the user presses the back button on Android.
28 | *
29 | * Anything not on this list will be a standard `back` action in
30 | * react-navigation.
31 | */
32 | const exitRoutes = ["main"];
33 | export const canExit = (routeName: string) => exitRoutes.includes(routeName);
34 |
--------------------------------------------------------------------------------
/app/navigators/navigation-utilities.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { BackHandler } from "react-native";
3 | import {
4 | PartialState,
5 | NavigationState,
6 | NavigationContainerRef,
7 | } from "@react-navigation/native";
8 |
9 | export const RootNavigation = {
10 | navigate(name: string) {
11 | name; // eslint-disable-line no-unused-expressions
12 | },
13 | goBack() {}, // eslint-disable-line @typescript-eslint/no-empty-function
14 | resetRoot(state?: PartialState | NavigationState) {}, // eslint-disable-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
15 | getRootState(): NavigationState {
16 | return {} as any;
17 | },
18 | };
19 |
20 | export const setRootNavigation = (
21 | ref: React.RefObject,
22 | ) => {
23 | for (const method in RootNavigation) {
24 | RootNavigation[method] = (...args: any) => {
25 | if (ref.current) {
26 | return ref.current[method](...args);
27 | }
28 | };
29 | }
30 | };
31 |
32 | /**
33 | * Gets the current screen from any navigation state.
34 | */
35 | export function getActiveRouteName(
36 | state: NavigationState | PartialState,
37 | ) {
38 | const route = state.routes[state.index];
39 |
40 | // Found the active route -- return the name
41 | if (!route.state) return route.name;
42 |
43 | // Recursive call to deal with nested routers
44 | return getActiveRouteName(route.state);
45 | }
46 |
47 | /**
48 | * Hook that handles Android back button presses and forwards those on to
49 | * the navigation or allows exiting the app.
50 | */
51 | export function useBackButtonHandler(
52 | ref: React.RefObject,
53 | canExit: (routeName: string) => boolean,
54 | ) {
55 | const canExitRef = useRef(canExit);
56 |
57 | useEffect(() => {
58 | canExitRef.current = canExit;
59 | }, [canExit]);
60 |
61 | useEffect(() => {
62 | // We'll fire this when the back button is pressed on Android.
63 | const onBackPress = () => {
64 | const navigation = ref.current;
65 |
66 | if (navigation == null) {
67 | return false;
68 | }
69 |
70 | // grab the current route
71 | const routeName = getActiveRouteName(navigation.getRootState());
72 |
73 | // are we allowed to exit?
74 | if (canExitRef.current(routeName)) {
75 | // let the system know we've not handled this event
76 | return false;
77 | }
78 |
79 | // we can't exit, so let's turn this into a back action
80 | if (navigation.canGoBack()) {
81 | navigation.goBack();
82 |
83 | return true;
84 | }
85 |
86 | return false;
87 | };
88 |
89 | // Subscribe when we come to life
90 | BackHandler.addEventListener("hardwareBackPress", onBackPress);
91 |
92 | // Unsubscribe when we're done
93 | return () =>
94 | BackHandler.removeEventListener("hardwareBackPress", onBackPress);
95 | }, [ref]);
96 | }
97 |
98 | /**
99 | * Custom hook for persisting navigation state.
100 | */
101 | export function useNavigationPersistence(storage: any, persistenceKey: string) {
102 | const [initialNavigationState, setInitialNavigationState] = useState();
103 | const [isRestoringNavigationState, setIsRestoringNavigationState] = useState(
104 | true,
105 | );
106 |
107 | const routeNameRef = useRef();
108 | const onNavigationStateChange = (state: any) => {
109 | const previousRouteName = routeNameRef.current;
110 | const currentRouteName = getActiveRouteName(state);
111 |
112 | if (previousRouteName !== currentRouteName) {
113 | // track screens.
114 | // __DEV__ && console.tron.log(currentRouteName)
115 | }
116 |
117 | // Save the current route name for later comparision
118 | routeNameRef.current = currentRouteName;
119 |
120 | // Persist state to storage
121 | storage.save(persistenceKey, state);
122 | };
123 |
124 | const restoreState = async () => {
125 | try {
126 | const state = await storage.load(persistenceKey);
127 | if (state) setInitialNavigationState(state);
128 | } finally {
129 | setIsRestoringNavigationState(false);
130 | }
131 | };
132 |
133 | useEffect(() => {
134 | if (isRestoringNavigationState) restoreState();
135 | }, [isRestoringNavigationState]);
136 |
137 | return { onNavigationStateChange, restoreState, initialNavigationState };
138 | }
139 |
--------------------------------------------------------------------------------
/app/navigators/root-navigator.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | NavigationContainer,
4 | NavigationContainerRef,
5 | } from "@react-navigation/native";
6 | import { createStackNavigator } from "@react-navigation/stack";
7 | import { MainNavigator } from "./main-navigator";
8 | import { AuthNavigator } from "./auth-navigator";
9 | import { connect } from "react-redux";
10 |
11 | export type RootParamList = {
12 | mainStack: undefined;
13 | authStack: undefined;
14 | };
15 |
16 | const Stack = createStackNavigator();
17 |
18 | const RootStack = (props: any) => {
19 | return (
20 |
26 | {props.isSignedIn ? (
27 |
28 | ) : (
29 |
30 | )}
31 |
32 | );
33 | };
34 |
35 | const mapStateToProps = (state: any) => ({
36 | isSignedIn: state.auth.user,
37 | });
38 |
39 | const ConnectedRootStack = connect(mapStateToProps)(RootStack);
40 |
41 | export const RootNavigator = React.forwardRef<
42 | NavigationContainerRef,
43 | Partial>
44 | >((props, ref) => {
45 | return (
46 |
47 |
48 |
49 | );
50 | });
51 |
52 | RootNavigator.displayName = "RootNavigator";
53 |
--------------------------------------------------------------------------------
/app/reducers/auth/auth.ts:
--------------------------------------------------------------------------------
1 | import { Auth } from "../../types";
2 | import { Action } from "../../actions/auth/types";
3 |
4 | const authReducerDefaultState: Readonly = {
5 | user: null,
6 | status: "idle",
7 | error: null,
8 | };
9 |
10 | export default (state = authReducerDefaultState, action: Action): Auth => {
11 | switch (action.type) {
12 | case "SIGN_IN":
13 | return { ...state, user: action.payload.user };
14 | case "SIGN_OUT":
15 | return { ...state, user: null };
16 | case "UPDATE_STATUS":
17 | return { ...state, status: action.payload.status };
18 | case "UPDATE_ERROR":
19 | return { ...state, error: action.payload.error };
20 | case "UPDATE_USER_DATA":
21 | return { ...state, user: action.payload.user };
22 | default:
23 | return state;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/app/reducers/filters/filters.ts:
--------------------------------------------------------------------------------
1 | import { Filters } from "../../types";
2 | import { Action } from "../../actions/filters/types";
3 |
4 | const filtersReducerDefaultState: Readonly = {
5 | filterBy: "all",
6 | sortBy: "nameAsc",
7 | };
8 |
9 | export default (state = filtersReducerDefaultState, action: Action) => {
10 | switch (action.type) {
11 | case "UPDATE_FILTER_BY":
12 | return { ...state, filterBy: action.payload.type };
13 | case "UPDATE_SORT_BY":
14 | return { ...state, sortBy: action.payload.type };
15 | default:
16 | return state;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/app/reducers/todos/todos.test.ts:
--------------------------------------------------------------------------------
1 | import todosReducer from "./todos";
2 | import * as actions from "../../actions/todos/todos";
3 | import { todos } from "../../test/fixtures";
4 | import { TodoData, Updates } from "../../actions/todos/types";
5 |
6 | describe("Todo reducer", () => {
7 | it("Add new todo", () => {
8 | const todo: TodoData = {
9 | type: "work",
10 | description: "YOSI BEZALHEL",
11 | isCompleted: true,
12 | };
13 | const updatedTodos = todosReducer(todos, actions.addTodo(todo));
14 | expect(updatedTodos).toEqual([
15 | ...todos,
16 | {
17 | ...todo,
18 | id: expect.any(String),
19 | },
20 | ]);
21 | });
22 |
23 | it("Removes a todo", () => {
24 | const testId = todos[3].id;
25 | const updatedTodos = todosReducer(todos, actions.removeTodo(testId));
26 | expect(updatedTodos).toEqual(todos.filter(todo => todo.id !== testId));
27 | });
28 |
29 | it("Update a todo", () => {
30 | const testId = todos[3].id;
31 | const testUpdates: Updates = {
32 | description: "lalalaa",
33 | };
34 | const updatedTodos = todosReducer(
35 | todos,
36 | actions.updateTodo(testId, testUpdates),
37 | );
38 | expect(updatedTodos).toEqual([
39 | ...todos.slice(0, 3),
40 | { ...todos[3], ...testUpdates },
41 | ...todos.slice(4),
42 | ]);
43 | });
44 | });
45 |
--------------------------------------------------------------------------------
/app/reducers/todos/todos.ts:
--------------------------------------------------------------------------------
1 | import { Todo } from "../../types";
2 | import { Action } from "../../actions/todos/types";
3 |
4 | const todosReducerDefaultState: Readonly = [];
5 |
6 | export default (state = todosReducerDefaultState, action: Action) => {
7 | switch (action.type) {
8 | case "SET_TODOS":
9 | return [...action.payload.todos];
10 | case "ADD_TODO":
11 | return [...state, { ...action.payload.todo }];
12 | case "REMOVE_TODO":
13 | return state.filter(({ id }) => id !== action.payload.id);
14 | case "UPDATE_TODO": {
15 | return state.map(todo => {
16 | if (todo.id === action.payload.id) {
17 | return Object.assign({}, todo, action.payload.updates);
18 | }
19 | return todo;
20 | });
21 | }
22 | default:
23 | return state;
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/app/screens/home/home-screen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import TodoList from "../../components/todo-list/todo-list";
3 | import Heading from "../../components/heading/heading";
4 | import NewTodoForm from "../../components/new-todo-form/new-todo-form";
5 | import FiltersForm from "../../components/filters-form/filters-form";
6 | import Screen from "../../components/screen/screen";
7 | import { startSignOut } from "../../actions/auth/auth";
8 | import { connect } from "react-redux";
9 | import { spacing } from "../../theme";
10 | import { translate } from "../../i18n";
11 | import styled from "@emotion/native";
12 | import Button from "../../components/button/button";
13 | import { HomeProps } from "./home.props";
14 |
15 | const Container = styled.View(() => ({
16 | flex: 1,
17 | }));
18 |
19 | const WelcomeTitle = styled(Heading)(() => ({
20 | paddingHorizontal: spacing[5],
21 | marginTop: spacing[5],
22 | }));
23 |
24 | const LogoutButton = styled(Button)(() => ({
25 | marginHorizontal: spacing[5],
26 | marginVertical: spacing[3],
27 | }));
28 |
29 | function HomeScreen(props: HomeProps) {
30 | return (
31 |
32 |
33 | props.signOut()}>
34 | Log Out
35 |
36 | {`${translate("homeScreen.title")}, ${
37 | props.name
38 | }!`}
39 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
47 | const mapDispatchToProps = (dispatch: any) => ({
48 | signOut: () => dispatch(startSignOut()),
49 | });
50 |
51 | const mapStateToProps = (state: any) => {
52 | if (!state.auth.user) return {};
53 |
54 | return {
55 | name: state.auth.user.email
56 | ? state.auth.user.email.split("@")[0]
57 | : "anonymous user",
58 | };
59 | };
60 |
61 | export default connect(mapStateToProps, mapDispatchToProps)(HomeScreen);
62 |
--------------------------------------------------------------------------------
/app/screens/home/home.props.ts:
--------------------------------------------------------------------------------
1 | export interface HomeProps {
2 | /**
3 | * The user name
4 | */
5 | name: string;
6 | /**
7 | * A callback to sign out from the app
8 | */
9 | signOut: () => void;
10 | /**
11 | * react-navigation navigation prop
12 | */
13 | navigation: any;
14 | }
15 |
--------------------------------------------------------------------------------
/app/screens/index.ts:
--------------------------------------------------------------------------------
1 | import HomeScreen from "./home/home-screen";
2 | import WelcomeScreen from "./welcome/welcome-screen";
3 | import SignInWithEmailScreen from "./sign-in-with-email/sign-in-with-email-screen";
4 | import SignUpWithEmailScreen from "./sign-up-with-email/sign-up-with-email-screen";
5 |
6 | export {
7 | HomeScreen,
8 | WelcomeScreen,
9 | SignInWithEmailScreen,
10 | SignUpWithEmailScreen,
11 | };
12 |
--------------------------------------------------------------------------------
/app/screens/sign-in-with-email/sign-in-with-email-screen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TouchableOpacity } from "react-native";
3 | import Heading from "../../components/heading/heading";
4 | import Input from "../../components/input/input";
5 | import Screen from "../../components/screen/screen";
6 | import LoadingButton from "../../components/loading-button/loading-button";
7 | import { startSignIn, updateError } from "../../actions/auth/auth";
8 | import { connect } from "react-redux";
9 | import { translate } from "../../i18n";
10 | import { spacing } from "../../theme";
11 | import styled from "@emotion/native";
12 | import { SignInWithEmailProps } from "./sign-in-with-email";
13 |
14 | const Container = styled.View({
15 | paddingHorizontal: spacing[5],
16 | });
17 |
18 | const Title = styled(Heading)({
19 | paddingVertical: spacing[3],
20 | });
21 |
22 | const SignInInput = styled(Input)({
23 | marginVertical: spacing[2],
24 | });
25 |
26 | const SignInButton = styled(LoadingButton)({
27 | paddingVertical: spacing[3],
28 | });
29 |
30 | const SignUpMessage = styled.View({
31 | justifyContent: "center",
32 | flexDirection: "row",
33 | paddingVertical: spacing[6],
34 | });
35 |
36 | const SignUpText = styled.Text({
37 | fontSize: spacing[4],
38 | });
39 |
40 | const SignUpButton = styled.Text(props => ({
41 | marginLeft: spacing[1],
42 | color: props.theme.text[300],
43 | fontSize: spacing[4],
44 | }));
45 |
46 | function SignInWithEmailScreen(props: SignInWithEmailProps) {
47 | const [email, setEmail] = React.useState("");
48 | const [password, setPassword] = React.useState("");
49 |
50 | return (
51 |
52 |
53 | {translate("signInWithEmailScreen.title")}
54 |
64 |
72 | props.signIn(email, password)}
76 | disabled={!email || !password}
77 | >
78 | {translate("common.signIn")}
79 |
80 |
81 |
82 | {translate("signInWithEmailScreen.signUpMessage")}
83 |
84 | {
86 | props.clearError();
87 | props.navigation.push("SignUpWithEmail");
88 | }}
89 | >
90 | {translate("common.signUp")}
91 |
92 |
93 |
94 |
95 | );
96 | }
97 |
98 | const mapDispatchToProps = (dispatch: any) => ({
99 | signIn: (email: string, password: string) =>
100 | dispatch(startSignIn(email, password)),
101 | clearError: () => dispatch(updateError(null)),
102 | });
103 |
104 | const mapStateToProps = (state: any) => ({
105 | isLoading: state.auth.status === "loading",
106 | error: state.auth.error,
107 | });
108 |
109 | export default connect(
110 | mapStateToProps,
111 | mapDispatchToProps,
112 | )(SignInWithEmailScreen);
113 |
--------------------------------------------------------------------------------
/app/screens/sign-in-with-email/sign-in-with-email.ts:
--------------------------------------------------------------------------------
1 | export interface SignInWithEmailProps {
2 | /**
3 | * Is auth is loading
4 | */
5 | isLoading: boolean;
6 | /**
7 | * An error string or null if there is any
8 | */
9 | error: string | null;
10 | /**
11 | * A callback to sign in the user
12 | */
13 | signIn: (email: string, password: string) => void;
14 | /**
15 | * A callback to clear any error on the redux store
16 | */
17 | clearError: () => void;
18 | /**
19 | * react-navigation navigation prop
20 | */
21 | navigation: any;
22 | }
23 |
--------------------------------------------------------------------------------
/app/screens/sign-up-with-email/sign-up-with-email-screen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useCredentialsFields } from "../../hooks/useCredentialsFields";
3 | import Heading from "../../components/heading/heading";
4 | import Input from "../../components/input/input";
5 | import Screen from "../../components/screen/screen";
6 | import LoadingButton from "../../components/loading-button/loading-button";
7 | import { startSignUp } from "../../actions/auth/auth";
8 | import { connect } from "react-redux";
9 | import { spacing } from "../../theme";
10 | import { translate } from "../../i18n";
11 | import styled from "@emotion/native";
12 | import { SignUpWithEmailProps } from "./sign-up-with-email";
13 |
14 | const Container = styled.View({
15 | paddingHorizontal: spacing[5],
16 | });
17 |
18 | const Title = styled(Heading)({
19 | paddingVertical: spacing[3],
20 | });
21 |
22 | const SignUpInput = styled(Input)({
23 | marginVertical: spacing[2],
24 | });
25 |
26 | const SignUpButton = styled(LoadingButton)({
27 | paddingVertical: spacing[3],
28 | });
29 |
30 | function SignUpWithEmailScreen(props: SignUpWithEmailProps) {
31 | const credentialsFields = useCredentialsFields();
32 |
33 | return (
34 |
35 |
36 | {translate("signUpWithEmailScreen.title")}
37 |
51 |
65 |
69 | props.signUp(
70 | credentialsFields.email.value,
71 | credentialsFields.password.value,
72 | )
73 | }
74 | disabled={
75 | !credentialsFields.email.value ||
76 | !credentialsFields.password.value ||
77 | !credentialsFields.password.isValid ||
78 | !credentialsFields.email.isValid
79 | }
80 | >
81 | {translate("common.signUp")}
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | const mapDispatchToProps = (dispatch: any) => ({
89 | signUp: (email: string, password: string) =>
90 | dispatch(startSignUp(email, password)),
91 | });
92 |
93 | const mapStateToProps = (state: any) => ({
94 | isLoading: state.auth.status === "loading",
95 | error: state.auth.error,
96 | });
97 |
98 | export default connect(
99 | mapStateToProps,
100 | mapDispatchToProps,
101 | )(SignUpWithEmailScreen);
102 |
--------------------------------------------------------------------------------
/app/screens/sign-up-with-email/sign-up-with-email.ts:
--------------------------------------------------------------------------------
1 | export interface SignUpWithEmailProps {
2 | /**
3 | * Is auth is loading
4 | */
5 | isLoading: boolean;
6 | /**
7 | * An error string or null if there is any
8 | */
9 | error: string | null;
10 | /**
11 | * A callback to sign in the user
12 | */
13 | signUp: (email: string, password: string) => void;
14 | /**
15 | * A callback to clear any error on the redux store
16 | */
17 | clearError: () => void;
18 | /**
19 | * react-navigation navigation prop
20 | */
21 | navigation: any;
22 | }
23 |
--------------------------------------------------------------------------------
/app/screens/welcome/welcome-screen.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 | import Heading from "../../components/heading/heading";
4 | import Screen from "../../components/screen/screen";
5 | import Button from "../../components/button/button";
6 | import {
7 | startSignInAnonymously,
8 | startSignInWithFacebook,
9 | startSignInWithGoogle,
10 | updateError,
11 | } from "../../actions/auth/auth";
12 | import ErrorMessage from "../../components/error-message/error-message";
13 | import { spacing } from "../../theme";
14 | import { translate } from "../../i18n";
15 | import styled from "@emotion/native";
16 | import { WelcomeProps } from "./welcome";
17 |
18 | const Container = styled.View({
19 | paddingHorizontal: spacing[5],
20 | });
21 |
22 | const WelcomeTitle = styled(Heading)({
23 | paddingVertical: spacing[5],
24 | });
25 |
26 | const LoginButton = styled(Button)({
27 | marginVertical: spacing[3],
28 | });
29 |
30 | const Error = styled.View({
31 | paddingVertical: spacing[5],
32 | alignSelf: "center",
33 | });
34 |
35 | function WelcomeScreen(props: WelcomeProps) {
36 | return (
37 |
38 |
39 | {translate("welcomeScreen.title")}
40 | {
42 | props.clearError();
43 | props.navigation.push("SignInWithEmail");
44 | }}
45 | kind="primary"
46 | >
47 | {translate("welcomeScreen.signInMethods.email")}
48 |
49 |
50 | {translate("welcomeScreen.signInMethods.anonymous")}
51 |
52 |
53 | {translate("welcomeScreen.signInMethods.gmail")}
54 |
55 |
56 | {translate("welcomeScreen.signInMethods.facebook")}
57 |
58 | {props.error && (
59 |
60 | {props.error}
61 |
62 | )}
63 |
64 |
65 | );
66 | }
67 |
68 | const mapDispatchToProps = (dispatch: any) => ({
69 | signInWithFacebook: () => dispatch(startSignInWithFacebook()),
70 | signInWithGoogle: () => dispatch(startSignInWithGoogle()),
71 | signInAnonymously: () => dispatch(startSignInAnonymously()),
72 | clearError: () => dispatch(updateError(null)),
73 | });
74 |
75 | const mapStateToProps = (state: any) => ({
76 | error: state.auth.error,
77 | });
78 |
79 | export default connect(mapStateToProps, mapDispatchToProps)(WelcomeScreen);
80 |
--------------------------------------------------------------------------------
/app/screens/welcome/welcome.ts:
--------------------------------------------------------------------------------
1 | export interface WelcomeProps {
2 | /**
3 | * Is auth is loading
4 | */
5 | isLoading: boolean;
6 | /**
7 | * An error string or null if there is any
8 | */
9 | error: string | null;
10 | /**
11 | * A callback to sign in the user
12 | */
13 | signUp: (email: string, password: string) => void;
14 | /**
15 | * A callback to sign in the user anonymously
16 | */
17 | signInAnonymously: () => void;
18 | /**
19 | * A callback to sign in the user with google
20 | */
21 | signInWithGoogle: () => void;
22 | /**
23 | * A callback to sign in the user with facebook
24 | */
25 | signInWithFacebook: () => void;
26 | /**
27 | * A callback to clear any error on the redux store
28 | */
29 | clearError: () => void;
30 | /**
31 | * react-navigation navigation prop
32 | */
33 | navigation: any;
34 | }
35 |
--------------------------------------------------------------------------------
/app/selectors/todos.ts:
--------------------------------------------------------------------------------
1 | import { Todo, Filters } from "../types";
2 |
3 | export function filterTodos(todos: Todo[], filters: Filters) {
4 | const { filterBy, sortBy } = filters;
5 |
6 | const sortedTodos = [...todos].sort((todoOne: Todo, todoTwo: Todo) => {
7 | if (todoOne.description < todoTwo.description) {
8 | return sortBy === "nameAsc" ? -1 : 1;
9 | } else if (todoOne.description > todoTwo.description) {
10 | return sortBy === "nameAsc" ? 1 : -1;
11 | } else {
12 | return 0;
13 | }
14 | });
15 |
16 | const filteredTodos =
17 | filterBy === "all"
18 | ? sortedTodos
19 | : sortedTodos.filter((todo: Todo) => todo.type === filterBy);
20 |
21 | return filteredTodos;
22 | }
23 |
24 | export function calculateTodosCount(todos: Array) {
25 | const todosCount: any = {};
26 | ["all", "personal", "work"].forEach(type => {
27 | todosCount[type] = {};
28 | todosCount[type].total = todos.filter(
29 | (todo: Todo) => todo.type === type || type === "all",
30 | ).length;
31 | todosCount[type].completed = todos.filter(
32 | (todo: Todo) =>
33 | (todo.type === type || type === "all") && todo.isCompleted,
34 | ).length;
35 | });
36 |
37 | return todosCount;
38 | }
39 |
--------------------------------------------------------------------------------
/app/services/auth-api.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FACEBOOK_APP_ID,
3 | GOOGLE_IOS_CLIENT_ID,
4 | GOOGLE_ANDROID_CLIENT_ID,
5 | } from "@env";
6 | import * as Facebook from "expo-facebook";
7 | import * as Google from "expo-google-app-auth";
8 | import { to } from "../utils/to";
9 | import { firebase } from "../firebase";
10 |
11 | export async function getFacebookToken() {
12 | await Facebook.initializeAsync({
13 | appId: FACEBOOK_APP_ID,
14 | appName: "regnite-todo",
15 | });
16 |
17 | const [error, data] = await to(
18 | Facebook.logInWithReadPermissionsAsync({
19 | permissions: ["public_profile", "email"],
20 | }),
21 | );
22 |
23 | if (error) {
24 | throw new Error(error.message);
25 | }
26 |
27 | if (data.type === "success") {
28 | return data.token;
29 | } else {
30 | throw new Error("canceled");
31 | }
32 | }
33 |
34 | export async function getGoogleToken() {
35 | const [error, result] = await to(
36 | Google.logInAsync({
37 | iosClientId: GOOGLE_IOS_CLIENT_ID,
38 | androidClientId: GOOGLE_ANDROID_CLIENT_ID,
39 | scopes: ["profile", "email"],
40 | }),
41 | );
42 |
43 | if (error) {
44 | throw new Error(error.message);
45 | }
46 |
47 | if (result.type === "success") {
48 | return result.accessToken;
49 | } else {
50 | throw new Error("canceled");
51 | }
52 | }
53 |
54 | export async function signInWithFacebook(token: string) {
55 | const credentials = firebase.auth.FacebookAuthProvider.credential(token);
56 | return await to(firebase.auth().signInWithCredential(credentials));
57 | }
58 |
59 | export async function signInWithGoogle(accessToken: string) {
60 | const credentials = firebase.auth.GoogleAuthProvider.credential(
61 | null,
62 | accessToken,
63 | );
64 | return await to(firebase.auth().signInWithCredential(credentials));
65 | }
66 |
67 | export async function signInAnonymously() {
68 | return await to(firebase.auth().signInAnonymously());
69 | }
70 |
71 | export async function signInWithEmail(email: string, password: string) {
72 | return await to(firebase.auth().signInWithEmailAndPassword(email, password));
73 | }
74 |
75 | export async function signUpWithEmail(email: string, password: string) {
76 | return await to(
77 | firebase.auth().createUserWithEmailAndPassword(email, password),
78 | );
79 | }
80 |
81 | export function signOut() {
82 | firebase.auth().signOut();
83 | }
84 |
--------------------------------------------------------------------------------
/app/services/todos-api.ts:
--------------------------------------------------------------------------------
1 | import { TodoData, Updates } from "../actions/todos/types";
2 | import { Todo } from "../types";
3 | import { database } from "../firebase";
4 |
5 | export async function create(uid: string, todo: TodoData) {
6 | const ref = await database.ref(`users/${uid}/todos`).push(todo);
7 | return ref.key;
8 | }
9 |
10 | export async function read(uid: string) {
11 | const snapshot = await database.ref(`users/${uid}/todos`).once("value");
12 | const todos: Array = [];
13 |
14 | snapshot.forEach(childSnapshot => {
15 | todos.push({
16 | id: childSnapshot.key,
17 | ...childSnapshot.val(),
18 | });
19 | });
20 |
21 | return todos;
22 | }
23 |
24 | export function update(uid: string, id: string, updates: Updates) {
25 | return database.ref(`users/${uid}/todos/${id}`).update(updates);
26 | }
27 |
28 | export function remove(uid: string, id: string) {
29 | return database.ref(`users/${uid}/todos/${id}`).remove();
30 | }
31 |
--------------------------------------------------------------------------------
/app/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from "redux";
2 | import { persistStore, persistReducer } from "redux-persist";
3 | import AsyncStorage from "@react-native-async-storage/async-storage";
4 | import rootReducer from "./rootReducer";
5 | import middlewares from "./middlewares";
6 |
7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
8 |
9 | const persistConfig = {
10 | key: "root",
11 | storage: AsyncStorage,
12 | };
13 |
14 | const persistedReducer = persistReducer(persistConfig, rootReducer);
15 |
16 | export default () => {
17 | const store = createStore(
18 | persistedReducer,
19 | composeEnhancers(applyMiddleware(...middlewares)),
20 | );
21 | const persistor = persistStore(store);
22 |
23 | return { store, persistor };
24 | };
25 |
--------------------------------------------------------------------------------
/app/store/middlewares.js:
--------------------------------------------------------------------------------
1 | import ReduxThunk from "redux-thunk";
2 |
3 | export default [ReduxThunk];
4 |
--------------------------------------------------------------------------------
/app/store/rootReducer.js:
--------------------------------------------------------------------------------
1 | import todosReducer from "../reducers/todos/todos";
2 | import filtersReducer from "../reducers/filters/filters";
3 | import authReducer from "../reducers/auth/auth";
4 | import { combineReducers } from "redux";
5 |
6 | export default combineReducers({
7 | todos: todosReducer,
8 | filters: filtersReducer,
9 | auth: authReducer,
10 | });
11 |
--------------------------------------------------------------------------------
/app/test/fixtures/index.ts:
--------------------------------------------------------------------------------
1 | export { todos } from "./todos";
2 |
--------------------------------------------------------------------------------
/app/test/fixtures/todos.ts:
--------------------------------------------------------------------------------
1 | import { Todo } from "../../types";
2 |
3 | export const todos: Array = [
4 | {
5 | type: "personal",
6 | description: "GUY RONN",
7 | isCompleted: true,
8 | id: "122423234",
9 | },
10 | {
11 | type: "work",
12 | description: "YOSI BEZALHEL",
13 | isCompleted: true,
14 | id: "131",
15 | },
16 | {
17 | type: "personal",
18 | description: "Frank Schultz",
19 | isCompleted: false,
20 | id: "2363",
21 | },
22 | {
23 | type: "work",
24 | description: "Eunice French",
25 | isCompleted: false,
26 | id: "2333",
27 | },
28 | {
29 | type: "work",
30 | description: "Craig Newman",
31 | isCompleted: true,
32 | id: "2443",
33 | },
34 | {
35 | type: "personal",
36 | description: "Nettie Wolfe",
37 | isCompleted: true,
38 | id: "2263",
39 | },
40 | {
41 | type: "personal",
42 | description: "Julian Bennett",
43 | isCompleted: false,
44 | id: "277",
45 | },
46 | ];
47 |
--------------------------------------------------------------------------------
/app/theme/colors.ts:
--------------------------------------------------------------------------------
1 | import { Green, Text, Neutral, Primary, Red, Yellow } from "../types/colors";
2 |
3 | export const primary: Primary = {
4 | 100: "#63A789",
5 | 200: "#509D53",
6 | 300: "#0C5844",
7 | 400: "#F7611D",
8 | 500: "#F85131",
9 | };
10 |
11 | export const text: Text = {
12 | 100: "#1D1D1C",
13 | 200: "#41413D",
14 | 300: "#777774",
15 | 400: "#EEEEEE",
16 | 500: "#D4D4D4",
17 | 600: "#A1A1A1",
18 | };
19 |
20 | export const neutral: Neutral = {
21 | 100: "#F8E7D4",
22 | 200: "#F8E3CA",
23 | 300: "#F6D8AD",
24 | 400: "#181E20",
25 | 500: "#111516",
26 | 600: "#030404",
27 | };
28 |
29 | export const green: Green = {
30 | 100: "#529e66",
31 | 200: "#367b48",
32 | 300: "#276738",
33 | };
34 |
35 | export const yellow: Yellow = {
36 | 100: "#e1c542",
37 | 200: "#cab23f",
38 | 300: "#b49e35",
39 | };
40 |
41 | export const red: Red = {
42 | 100: "#d0454c",
43 | 200: "#b54248",
44 | 300: "#95353a",
45 | };
46 |
--------------------------------------------------------------------------------
/app/theme/fonts/index.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | "Merriweather-Regular": require("../../../assets/fonts/Merriweather-Regular.ttf"),
3 | "Merriweather-Bold": require("../../../assets/fonts/Merriweather-Bold.ttf"),
4 | };
5 |
--------------------------------------------------------------------------------
/app/theme/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./colors";
2 | export * from "./spacing";
3 | export * from "./typography";
4 | export * from "./timing";
5 |
--------------------------------------------------------------------------------
/app/theme/palette.ts:
--------------------------------------------------------------------------------
1 | import { Palette } from "../types/palette";
2 | import * as colors from "./colors";
3 |
4 | export const palette: Palette = {
5 | ...colors,
6 | };
7 |
--------------------------------------------------------------------------------
/app/theme/spacing.ts:
--------------------------------------------------------------------------------
1 | export const spacing = [0, 4, 8, 12, 16, 24, 32, 48, 64];
2 |
--------------------------------------------------------------------------------
/app/theme/themes.ts:
--------------------------------------------------------------------------------
1 | import { ThemeInterface } from "../types/theme";
2 | import { palette } from "./palette";
3 |
4 | export const lightTheme: ThemeInterface = {
5 | palette,
6 | transparent: "rgba(0, 0, 0, 0)",
7 | background: {
8 | 100: palette.neutral[100],
9 | 200: palette.neutral[200],
10 | 300: palette.neutral[300],
11 | },
12 | text: {
13 | 100: palette.text[100],
14 | 200: palette.text[200],
15 | 300: palette.text[300],
16 | },
17 | primary: palette.primary,
18 | error: palette.red,
19 | category: {
20 | work: palette.primary[100],
21 | personal: palette.primary[400],
22 | all: palette.text[100],
23 | },
24 | };
25 |
26 | export const darkTheme: ThemeInterface = {
27 | palette,
28 | transparent: "rgba(0, 0, 0, 0)",
29 | background: {
30 | 100: palette.neutral[400],
31 | 200: palette.neutral[500],
32 | 300: palette.neutral[600],
33 | },
34 | text: {
35 | 100: palette.text[400],
36 | 200: palette.text[500],
37 | 300: palette.text[600],
38 | },
39 | primary: palette.primary,
40 | error: palette.red,
41 | category: {
42 | work: palette.primary[100],
43 | personal: palette.primary[400],
44 | all: palette.text[100],
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/app/theme/timing.ts:
--------------------------------------------------------------------------------
1 | export const timing = {
2 | quick: 200,
3 | medium: 500,
4 | long: 1000,
5 | };
6 |
--------------------------------------------------------------------------------
/app/theme/typography.ts:
--------------------------------------------------------------------------------
1 | import { Platform } from "react-native";
2 |
3 | export const typography = {
4 | primary: { bold: "Merriweather-Bold", regular: "Merriweather-Regular" },
5 | secondary: Platform.select({ ios: "Arial", android: "sans-serif" }),
6 | code: Platform.select({ ios: "Courier", android: "monospace" }),
7 | };
8 |
--------------------------------------------------------------------------------
/app/types/auth.ts:
--------------------------------------------------------------------------------
1 | import firebase from "firebase/app";
2 | export type Status = "idle" | "loading" | "succeeded" | "failed";
3 | export type Error = string | null;
4 | export type User = firebase.User | null;
5 |
6 | export interface Auth {
7 | user: User;
8 | status: Status;
9 | error: Error;
10 | }
11 |
--------------------------------------------------------------------------------
/app/types/colors.ts:
--------------------------------------------------------------------------------
1 | export interface Primary {
2 | 100: string;
3 | 200: string;
4 | 300: string;
5 | 400: string;
6 | 500: string;
7 | }
8 |
9 | export interface Text {
10 | 100: string;
11 | 200: string;
12 | 300: string;
13 | 400: string;
14 | 500: string;
15 | 600: string;
16 | }
17 |
18 | export interface Neutral {
19 | 100: string;
20 | 200: string;
21 | 300: string;
22 | 400: string;
23 | 500: string;
24 | 600: string;
25 | }
26 |
27 | export interface Green {
28 | 100: string;
29 | 200: string;
30 | 300: string;
31 | }
32 |
33 | export interface Yellow {
34 | 100: string;
35 | 200: string;
36 | 300: string;
37 | }
38 |
39 | export interface Red {
40 | 100: string;
41 | 200: string;
42 | 300: string;
43 | }
44 |
--------------------------------------------------------------------------------
/app/types/emotion.d.ts:
--------------------------------------------------------------------------------
1 | import "@emotion/react";
2 | import { ThemeInterface } from "./theme";
3 |
4 | declare module "@emotion/react" {
5 | export interface Theme extends ThemeInterface {}
6 | }
7 |
--------------------------------------------------------------------------------
/app/types/env.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@env" {
2 | export const FIREBASE_API_KEY: string;
3 | export const FIREBASE_AUTH_DOMAIN: string;
4 | export const FIREBASE_DATABASE_URL: string;
5 | export const FIREBASE_PROJECT_ID: string;
6 | export const FIREBASE_STORAGE_BUCKET: string;
7 | export const FIREBASE_MESSAGING_SENDER_ID: string;
8 | export const FIREBASE_APP_ID: string;
9 | export const FIREBASE_MEASUREMENT_ID: string;
10 | export const FACEBOOK_APP_ID: string;
11 | export const GOOGLE_IOS_CLIENT_ID: string;
12 | export const GOOGLE_ANDROID_CLIENT_ID: string;
13 | }
14 |
--------------------------------------------------------------------------------
/app/types/filters.ts:
--------------------------------------------------------------------------------
1 | export type Category = "all" | "work" | "personal";
2 | export type Sort = "nameDes" | "nameAsc";
3 |
4 | export interface Filters {
5 | filterBy: Category;
6 | sortBy: Sort;
7 | }
8 |
--------------------------------------------------------------------------------
/app/types/index.ts:
--------------------------------------------------------------------------------
1 | export { Todo } from "./todo";
2 | export { Filters } from "./filters";
3 | export { Auth } from "./auth";
4 |
--------------------------------------------------------------------------------
/app/types/palette.ts:
--------------------------------------------------------------------------------
1 | import { Green, Neutral, Primary, Red, Text, Yellow } from "./colors";
2 |
3 | export interface Palette {
4 | primary: Primary;
5 | text: Text;
6 | neutral: Neutral;
7 | red: Red;
8 | yellow: Yellow;
9 | green: Green;
10 | }
11 |
--------------------------------------------------------------------------------
/app/types/theme.ts:
--------------------------------------------------------------------------------
1 | import { Primary, Red } from "./colors";
2 | import { Palette } from "./palette";
3 |
4 | export interface ThemeInterface {
5 | palette: Palette;
6 | /**
7 | * Use sparingly as many layers of transparency
8 | * can cause older Android devices to slow down due
9 | * to the excessive compositing required
10 | * by their under-powered GPUs.
11 | */
12 | transparent: string;
13 | /**
14 | * The screen background.
15 | */
16 | background: {
17 | 100: string;
18 | 200: string;
19 | 300: string;
20 | };
21 | /**
22 | * The main tinting color.
23 | */
24 | primary: Primary;
25 | /**
26 | * The default color of text in many components.
27 | */
28 | text: {
29 | 100: string;
30 | 200: string;
31 | 300: string;
32 | };
33 | /**
34 | * Error messages and icons.
35 | */
36 | error: Red;
37 | /**
38 | * Colors for categories
39 | */
40 | category: {
41 | work: string;
42 | personal: string;
43 | all: string;
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/app/types/todo.ts:
--------------------------------------------------------------------------------
1 | export interface Todo {
2 | id: string;
3 | description: string;
4 | isCompleted: boolean;
5 | type: "personal" | "work";
6 | }
7 |
--------------------------------------------------------------------------------
/app/utils/delay.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A "modern" sleep statement.
3 | *
4 | * @param ms The number of milliseconds to wait.
5 | */
6 | export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
7 |
--------------------------------------------------------------------------------
/app/utils/ignore-warnings.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Ignore some yellowbox warnings. Some of these are for deprecated functions
3 | * that we haven't gotten around to replacing yet.
4 | */
5 | import { LogBox } from "react-native";
6 |
7 | // prettier-ignore
8 | LogBox.ignoreLogs([
9 | "Require cycle:",
10 | "Setting a timer"
11 | ])
12 |
--------------------------------------------------------------------------------
/app/utils/keychain.ts:
--------------------------------------------------------------------------------
1 | import * as ReactNativeKeychain from "react-native-keychain"
2 |
3 | /**
4 | * Saves some credentials securely.
5 | *
6 | * @param username The username
7 | * @param password The password
8 | * @param server The server these creds are for.
9 | */
10 | export async function save(username: string, password: string, server?: string) {
11 | if (server) {
12 | await ReactNativeKeychain.setInternetCredentials(server, username, password)
13 | return true
14 | } else {
15 | return ReactNativeKeychain.setGenericPassword(username, password)
16 | }
17 | }
18 |
19 | /**
20 | * Loads credentials that were already saved.
21 | *
22 | * @param server The server that these creds are for
23 | */
24 | export async function load(server?: string) {
25 | if (server) {
26 | const creds = await ReactNativeKeychain.getInternetCredentials(server)
27 | return {
28 | username: creds ? creds.username : null,
29 | password: creds ? creds.password : null,
30 | server,
31 | }
32 | } else {
33 | const creds = await ReactNativeKeychain.getGenericPassword()
34 | if (typeof creds === "object") {
35 | return {
36 | username: creds.username,
37 | password: creds.password,
38 | server: null,
39 | }
40 | } else {
41 | return {
42 | username: null,
43 | password: null,
44 | server: null,
45 | }
46 | }
47 | }
48 | }
49 |
50 | /**
51 | * Resets any existing credentials for the given server.
52 | *
53 | * @param server The server which has these creds
54 | */
55 | export async function reset(server?: string) {
56 | if (server) {
57 | await ReactNativeKeychain.resetInternetCredentials(server)
58 | return true
59 | } else {
60 | const result = await ReactNativeKeychain.resetGenericPassword()
61 | return result
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/utils/storage/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./storage"
2 |
--------------------------------------------------------------------------------
/app/utils/storage/storage.ts:
--------------------------------------------------------------------------------
1 | import AsyncStorage from "@react-native-async-storage/async-storage"
2 |
3 | /**
4 | * Loads a string from storage.
5 | *
6 | * @param key The key to fetch.
7 | */
8 | export async function loadString(key: string): Promise {
9 | try {
10 | return await AsyncStorage.getItem(key)
11 | } catch {
12 | // not sure why this would fail... even reading the RN docs I'm unclear
13 | return null
14 | }
15 | }
16 |
17 | /**
18 | * Saves a string to storage.
19 | *
20 | * @param key The key to fetch.
21 | * @param value The value to store.
22 | */
23 | export async function saveString(key: string, value: string): Promise {
24 | try {
25 | await AsyncStorage.setItem(key, value)
26 | return true
27 | } catch {
28 | return false
29 | }
30 | }
31 |
32 | /**
33 | * Loads something from storage and runs it thru JSON.parse.
34 | *
35 | * @param key The key to fetch.
36 | */
37 | export async function load(key: string): Promise {
38 | try {
39 | const almostThere = await AsyncStorage.getItem(key)
40 | return JSON.parse(almostThere)
41 | } catch {
42 | return null
43 | }
44 | }
45 |
46 | /**
47 | * Saves an object to storage.
48 | *
49 | * @param key The key to fetch.
50 | * @param value The value to store.
51 | */
52 | export async function save(key: string, value: any): Promise {
53 | try {
54 | await AsyncStorage.setItem(key, JSON.stringify(value))
55 | return true
56 | } catch {
57 | return false
58 | }
59 | }
60 |
61 | /**
62 | * Removes something from storage.
63 | *
64 | * @param key The key to kill.
65 | */
66 | export async function remove(key: string): Promise {
67 | try {
68 | await AsyncStorage.removeItem(key)
69 | } catch {}
70 | }
71 |
72 | /**
73 | * Burn it all to the ground.
74 | */
75 | export async function clear(): Promise {
76 | try {
77 | await AsyncStorage.clear()
78 | } catch {}
79 | }
80 |
--------------------------------------------------------------------------------
/app/utils/to.ts:
--------------------------------------------------------------------------------
1 | export function to(
2 | promise: Promise,
3 | errorExt?: any,
4 | ): Promise<[U, undefined] | [null, T]> {
5 | return promise
6 | .then<[null, T]>((data: T) => [null, data])
7 | .catch<[U, undefined]>((err: U) => {
8 | if (errorExt) {
9 | Object.assign(err, errorExt);
10 | }
11 |
12 | return [err, undefined];
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/app/utils/validate.ts:
--------------------------------------------------------------------------------
1 | export function validateEmail(email: string) {
2 | const re = /^(([^<>()[\]\\.,;:\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,}))$/;
3 | return re.test(String(email).toLowerCase());
4 | }
5 |
6 | export function validatePassword(password: string) {
7 | return (
8 | /[A-Z]/.test(password) &&
9 | /[a-z]/.test(password) &&
10 | /[0-9]/.test(password) &&
11 | password.length > 6
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/assets/fonts/Merriweather-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/assets/fonts/Merriweather-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Merriweather-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/assets/fonts/Merriweather-Regular.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/assets/images/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["babel-preset-expo"],
3 | env: {
4 | production: {},
5 | },
6 | plugins: [
7 | [
8 | "@babel/plugin-proposal-decorators",
9 | {
10 | legacy: true,
11 | },
12 | ],
13 | ["@babel/plugin-proposal-optional-catch-binding"],
14 | ["module:react-native-dotenv"],
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/ignite/templates/action/NAME.test.ts.ejs:
--------------------------------------------------------------------------------
1 | import * as actions from "./<%= props.kebabCaseName %>";
2 |
3 | describe("<%= props.PascalCaseName %> action generators", () => {
4 | it("Generate action", () => {
5 |
6 | });
7 | });
--------------------------------------------------------------------------------
/ignite/templates/action/NAME.ts.ejs:
--------------------------------------------------------------------------------
1 | export const actionGenerator = () => ({
2 | type: "ACTION_TYPE",
3 | payload: {},
4 | });
--------------------------------------------------------------------------------
/ignite/templates/action/types.ts.ejs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oneflex/regnite/8987bee7925980546758efebee6cc75b05415da0/ignite/templates/action/types.ts.ejs
--------------------------------------------------------------------------------
/ignite/templates/component/NAME.props.ts.ejs:
--------------------------------------------------------------------------------
1 | import { ViewStyle } from "react-native";
2 |
3 | export interface <%= props.pascalCaseName %>Props {
4 | /**
5 | * Container style overrides
6 | */
7 | style?: ViewStyle;
8 | }
9 |
--------------------------------------------------------------------------------
/ignite/templates/component/NAME.story.tsx.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { storiesOf } from "@storybook/react-native";
3 | import { StoryScreen, Story, UseCase, Docs, PropsData } from "../../../storybook/views";
4 | import <%= props.pascalCaseName %> from "./<%= props.kebabCaseName %>";
5 |
6 | declare let module: any;
7 |
8 | const description = `
9 | This is a <%= props.camelCaseName %> component.
10 | `;
11 |
12 | const propsData: PropsData = [
13 | ["style", "Container style overrides `ViewStyle`", "-"]
14 | ];
15 |
16 | storiesOf("<%= props.pascalCaseName %>", module)
17 | .addDecorator(fn => {fn()})
18 | .add("📖 Docs", () => (
19 |
24 | ))
25 | .add("Behaviour", () => (
26 |
27 |
28 | <<%= props.pascalCaseName %> />
29 |
30 |
31 | ));
32 |
--------------------------------------------------------------------------------
/ignite/templates/component/NAME.tsx.ejs:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { <%= props.pascalCaseName %>Props } from "./<%= props.kebabCaseName %>.props";
3 | import styled from "@emotion/native";
4 |
5 | const Container = styled.View(props => ({
6 | backgroundColor: props.theme.background[100],
7 | }));
8 |
9 | const Text = styled.Text(props => ({
10 | color: props.theme.text[100],
11 | }));
12 |
13 | const <%= props.pascalCaseName %>: React.FC<<%= props.pascalCaseName %>Props> = props => {
14 | return (
15 |
16 | Input
17 |
18 | );
19 | };
20 |
21 | export default <%= props.pascalCaseName %>;
22 |
23 |
--------------------------------------------------------------------------------
/ignite/templates/navigator/NAME-navigator.tsx.ejs:
--------------------------------------------------------------------------------
1 | import { StackNavigator } from "react-navigation"
2 | import {
3 | SomeScreen
4 | } from "../screens"
5 |
6 | export const <%= props.pascalCaseName %> = StackNavigator({
7 | })
--------------------------------------------------------------------------------
/ignite/templates/reducer/NAME.ts.ejs:
--------------------------------------------------------------------------------
1 | import { <%= props.pascalCaseName %> } from "../../types";
2 | import { action } from "../../actions/<%= props.pascalCaseName %>/types";
3 |
4 | const <%= props.camelCaseName %>ReducerDefaultState: Readonly<<%= props.camelCaseName %>[]> = [];
5 |
6 | export default (state = <%= props.camelCaseName %>ReducerDefaultState, action : Action) : <%= props.camelCaseName %> => {
7 | switch (action.type) {
8 | case "ACTION_TYPE":
9 |
10 | default:`
11 | return state;
12 | }
13 | };
--------------------------------------------------------------------------------
/ignite/templates/screen/NAME-screen.tsx.ejs:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { View, ViewStyle } from "react-native";
3 |
4 | const FULL: ViewStyle = { flex: 1 };
5 |
6 | function <%= props.pascalCaseName %>Screen() {
7 | return (
8 |
9 |
10 | );
11 | }
12 |
13 | export default <%= props.pascalCaseName %>Screen;
--------------------------------------------------------------------------------
/ignite/templates/screen/NAME.props.ts.ejs:
--------------------------------------------------------------------------------
1 | export interface <%= props.pascalCaseName %>Props {
2 | /**
3 | * react-navigation navigation prop
4 | */
5 | navigation: any;
6 | }
7 |
--------------------------------------------------------------------------------
/package.expo.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject",
9 | "lint": "eslint App.js app storybook test --fix --ext .js,.ts,.tsx && yarn format",
10 | "build:e2e": "detox build -c ios.sim.expo",
11 | "test:e2e": "./bin/downloadExpoApp.sh && detox test --configuration ios.sim.expo"
12 | },
13 | "dependencies": {
14 | "@expo/webpack-config": "^0.12.71",
15 | "expo": "40.0.1",
16 | "expo-status-bar": "~1.0.4",
17 | "query-string": "7.0.0",
18 | "react": "16.13.1",
19 | "react-native": "https://github.com/expo/react-native/archive/sdk-40.0.1.tar.gz"
20 | },
21 | "devDependencies": {
22 | "@types/react": "16.9.35",
23 | "@types/react-dom": "16.9.8",
24 | "@types/react-native": "0.63.2",
25 | "expo-detox-hook": "1.0.10",
26 | "detox-expo-helpers": "0.6.0"
27 | },
28 | "jest": {
29 | "transformIgnorePatterns": [
30 | "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|native-base|@storybook)"
31 | ]
32 | },
33 | "detox": {
34 | "configurations": {
35 | "ios.sim.expo": {
36 | "binaryPath": "bin/Exponent.app",
37 | "type": "ios.simulator",
38 | "name": "iPhone 11"
39 | },
40 | "ios.sim.debug": null,
41 | "ios.sim.release": null
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "expo start",
5 | "dev": "expo start",
6 | "test": "jest",
7 | "g": "npx ignite-cli generate",
8 | "lint": "eslint App.js app storybook test --fix --ext .js,.ts,.tsx && yarn format",
9 | "storybook": "start-storybook -p 9001 -c ./storybook",
10 | "adb": "adb reverse tcp:9090 tcp:9090 && adb reverse tcp:3000 tcp:3000 && adb reverse tcp:9001 tcp:9001 && adb reverse tcp:8081 tcp:8081",
11 | "postinstall": "",
12 | "build-ios": "react-native bundle --entry-file index.js --platform ios --dev false --bundle-output ios/main.jsbundle --assets-dest ios",
13 | "build-android": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res",
14 | "clean": "react-native-clean-project",
15 | "clean-all": "npx react-native clean-project-auto",
16 | "android": "expo start --android",
17 | "ios": "expo start --ios",
18 | "web": "expo start --web",
19 | "eject": "expo eject"
20 | },
21 | "dependencies": {
22 | "@emotion/native": "^11.0.0",
23 | "@emotion/react": "^11.4.1",
24 | "@expo/webpack-config": "~0.12.63",
25 | "@react-native-async-storage/async-storage": "^1.15.5",
26 | "@react-native-community/masked-view": "0.1.10",
27 | "@react-navigation/native": "5.9.3",
28 | "@react-navigation/stack": "5.12.8",
29 | "@storybook/addon-storyshots": "6.1.10",
30 | "@storybook/react-native": "5.3.23",
31 | "@storybook/react-native-server": "5.3.23",
32 | "@types/debounce": "^1.2.0",
33 | "@types/i18n-js": "^3.8.2",
34 | "@types/react-native-dotenv": "^0.2.0",
35 | "@types/uuid": "^8.3.1",
36 | "debounce": "^1.2.1",
37 | "dotenv": "^10.0.0",
38 | "expo": "^42.0.0",
39 | "expo-facebook": "~11.3.1",
40 | "expo-font": "~9.2.1",
41 | "expo-google-app-auth": "^8.2.2",
42 | "expo-localization": "~10.2.0",
43 | "expo-status-bar": "~1.0.4",
44 | "firebase": "8.2.3",
45 | "i18n-js": "^3.8.0",
46 | "react": "16.13.1",
47 | "react-native": "0.63.4",
48 | "react-native-gesture-handler": "~1.10.2",
49 | "react-native-keychain": "6.2.0",
50 | "react-native-markdown-display": "^7.0.0-alpha.2",
51 | "react-native-safe-area-context": "3.2.0",
52 | "react-native-screens": "~3.4.0",
53 | "react-native-svg": "12.1.1",
54 | "react-native-unimodules": "~0.14.5",
55 | "react-redux": "^7.2.4",
56 | "redux": "^4.1.0",
57 | "redux-persist": "^6.0.0",
58 | "redux-thunk": "^2.3.0"
59 | },
60 | "devDependencies": {
61 | "@babel/core": "~7.9.0",
62 | "@babel/plugin-proposal-decorators": "7.12.1",
63 | "@babel/plugin-proposal-optional-catch-binding": "7.12.1",
64 | "@babel/runtime": "^7.12.5",
65 | "@types/i18n-js": "^7.12.5",
66 | "@types/jest": "26.0.19",
67 | "@types/react": "~16.9.35",
68 | "@types/react-dom": "~16.9.8",
69 | "@types/react-native": "~0.63.2",
70 | "@types/react-test-renderer": "16.9.4",
71 | "@typescript-eslint/eslint-plugin": "4.10.0",
72 | "@typescript-eslint/parser": "4.10.0",
73 | "babel-jest": "26.6.3",
74 | "babel-loader": "8.2.2",
75 | "eslint": "7.15.0",
76 | "eslint-config-prettier": "7.0.0",
77 | "eslint-config-standard": "16.0.2",
78 | "eslint-plugin-import": "2.22.1",
79 | "eslint-plugin-node": "11.1.0",
80 | "eslint-plugin-promise": "4.2.1",
81 | "eslint-plugin-react": "7.21.5",
82 | "eslint-plugin-react-native": "3.10.0",
83 | "fbjs-scripts": "3.0.0",
84 | "jest": "^25.5.4",
85 | "jest-circus": "25.5.4",
86 | "jest-expo": "^42.0.0",
87 | "jetifier": "1.6.6",
88 | "npm-run-all": "4.1.5",
89 | "patch-package": "6.2.2",
90 | "postinstall-prepare": "1.0.1",
91 | "prettier": "2.2.1",
92 | "react-devtools-core": "4.10.1",
93 | "react-dom": "16.13.1",
94 | "react-native-clean-project": "^3.6.3",
95 | "react-native-dotenv": "^3.1.1",
96 | "react-native-web": "~0.13.12",
97 | "react-powerplug": "^1.0.0",
98 | "typescript": "~4.0.0"
99 | },
100 | "jest": {
101 | "preset": "jest-expo",
102 | "testPathIgnorePatterns": [
103 | "/node_modules/",
104 | "/e2e"
105 | ],
106 | "transformIgnorePatterns": [
107 | "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|native-base|@storybook)"
108 | ]
109 | },
110 | "prettier": {
111 | "printWidth": 80,
112 | "singleQuote": false,
113 | "trailingComma": "all",
114 | "semi": true,
115 | "arrowParens": "avoid"
116 | },
117 | "eslintConfig": {
118 | "root": true,
119 | "parser": "@typescript-eslint/parser",
120 | "extends": [
121 | "plugin:@typescript-eslint/recommended",
122 | "plugin:react/recommended",
123 | "plugin:react-native/all",
124 | "standard",
125 | "prettier",
126 | "prettier/@typescript-eslint"
127 | ],
128 | "plugins": [
129 | "@typescript-eslint",
130 | "react",
131 | "react-native"
132 | ],
133 | "parserOptions": {
134 | "ecmaFeatures": {
135 | "jsx": true
136 | },
137 | "project": "./tsconfig.json"
138 | },
139 | "settings": {
140 | "react": {
141 | "pragma": "React",
142 | "version": "detect"
143 | }
144 | },
145 | "globals": {
146 | "__DEV__": false,
147 | "jasmine": false,
148 | "beforeAll": false,
149 | "afterAll": false,
150 | "beforeEach": false,
151 | "afterEach": false,
152 | "test": false,
153 | "expect": false,
154 | "describe": false,
155 | "jest": false,
156 | "it": false
157 | },
158 | "rules": {
159 | "@typescript-eslint/ban-ts-ignore": 0,
160 | "@typescript-eslint/explicit-function-return-type": 0,
161 | "@typescript-eslint/explicit-member-accessibility": 0,
162 | "@typescript-eslint/explicit-module-boundary-types": 0,
163 | "@typescript-eslint/indent": 0,
164 | "@typescript-eslint/member-delimiter-style": 0,
165 | "@typescript-eslint/no-empty-interface": 0,
166 | "@typescript-eslint/no-explicit-any": 0,
167 | "@typescript-eslint/no-object-literal-type-assertion": 0,
168 | "@typescript-eslint/no-var-requires": 0,
169 | "comma-dangle": 0,
170 | "multiline-ternary": 0,
171 | "no-undef": 0,
172 | "no-unused-vars": 0,
173 | "no-use-before-define": "off",
174 | "quotes": 0,
175 | "react-native/no-raw-text": 0,
176 | "react/no-unescaped-entities": 0,
177 | "react/prop-types": "off",
178 | "space-before-function-paren": 0
179 | }
180 | },
181 | "main": "node_modules/expo/AppEntry.js"
182 | }
183 |
--------------------------------------------------------------------------------
/react-native.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | assets: ["./assets/fonts/"],
3 | }
4 |
--------------------------------------------------------------------------------
/storybook/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./storybook";
2 |
--------------------------------------------------------------------------------
/storybook/storybook-registry.ts:
--------------------------------------------------------------------------------
1 | require("../app/components/heading/heading.story");
2 | require("../app/components/input/input.story");
3 | require("../app/components/error-message/error-message.story");
4 | require("../app/components/button/button.story");
5 | require("../app/components/todo-item/todo-item.story");
6 | require("../app/components/checkbox/checkbox.story");
7 |
--------------------------------------------------------------------------------
/storybook/storybook.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { getStorybookUI, configure } from "@storybook/react-native";
3 | import ReactiveThemeProvider from "../app/components/reactive-theme-provider/reactive-theme-provider";
4 | import { darkTheme, lightTheme } from "../app/theme/themes";
5 | import { useFonts } from "expo-font";
6 | import fonts from "../app/theme/fonts";
7 |
8 | declare let module: any;
9 |
10 | configure(() => {
11 | require("./storybook-registry");
12 | }, module);
13 |
14 | const StorybookUI = getStorybookUI({
15 | port: 9001,
16 | host: "localhost",
17 | onDeviceUI: true,
18 | asyncStorage:
19 | require("@react-native-async-storage/async-storage").default || null,
20 | });
21 |
22 | export function StorybookUIRoot() {
23 | const [loaded] = useFonts(fonts);
24 |
25 | if (!loaded) return null;
26 |
27 | return (
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/storybook/views/docs.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/native";
2 | import * as React from "react";
3 | import { spacing, typography } from "../../app/theme";
4 | import Markdown from "react-native-markdown-display";
5 | import { useTheme } from "@emotion/react";
6 |
7 | export type PropsData = Array<
8 | [name: string, description: string, defaultValue: string]
9 | >;
10 |
11 | const Container = styled.View(props => ({
12 | backgroundColor: props.theme.background[100],
13 | paddingHorizontal: spacing[5],
14 | }));
15 |
16 | const Header = styled.View(() => ({
17 | paddingTop: spacing[5],
18 | paddingBottom: spacing[3],
19 | }));
20 |
21 | const Title = styled.Text(props => ({
22 | fontFamily: typography.primary.bold,
23 | fontSize: spacing[5],
24 | fontWeight: "600",
25 | color: props.theme.text[100],
26 | }));
27 |
28 | export interface DescriptionProps {
29 | /** Information about the component */
30 | children: string;
31 | }
32 |
33 | function Description(props: DescriptionProps) {
34 | const theme = useTheme();
35 | const style = {
36 | body: {
37 | paddingVertical: spacing[2],
38 | fontSize: spacing[4],
39 | color: theme.text[100],
40 | fontFamily: typography.primary.regular,
41 | },
42 | code_inline: {
43 | color: "#242322",
44 | },
45 | fence: {
46 | color: "#242322",
47 | },
48 | };
49 |
50 | return {props.children};
51 | }
52 |
53 | export interface PropsTableProps {
54 | /** trios of prop name, description and default value */
55 | propsData: PropsData;
56 | }
57 |
58 | function PropsTable(props: PropsTableProps) {
59 | const theme = useTheme();
60 | const style = {
61 | body: {
62 | paddingVertical: spacing[2],
63 | fontSize: spacing[4],
64 | color: theme.text[100],
65 | fontFamily: typography.primary.regular,
66 | },
67 | code_inline: {
68 | color: "#242322",
69 | },
70 | fence: {
71 | color: "#242322",
72 | },
73 | };
74 |
75 | const table = `
76 | | Name | Description | Default |
77 | |---- | ----------- | ------- |
78 | ${props.propsData.reduce(
79 | (accu, [name, description, defaultValue]) =>
80 | accu + `| ${name} | ${description} | \`${defaultValue}\` |\n`,
81 | "",
82 | )}
83 | `;
84 |
85 | return {table};
86 | }
87 |
88 | export interface DocsProps {
89 | /** The title. */
90 | title: string;
91 | /** Information about the component */
92 | description?: string;
93 | /** trios of prop name, description and default value */
94 | propsData: PropsData;
95 | }
96 |
97 | export function Docs(props: DocsProps) {
98 | return (
99 |
100 |
101 | {props.title}
102 | {props.description ? (
103 | {props.description}
104 | ) : null}
105 |
106 | Props
107 |
108 |
109 | );
110 | }
111 |
--------------------------------------------------------------------------------
/storybook/views/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./story-screen";
2 | export * from "./story";
3 | export * from "./use-case";
4 | export * from "./docs";
5 |
--------------------------------------------------------------------------------
/storybook/views/story-screen.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Platform } from "react-native";
3 | import styled from "@emotion/native";
4 | import { ScrollView } from "react-native-gesture-handler";
5 |
6 | const Container = styled.KeyboardAvoidingView(props => ({
7 | backgroundColor: props.theme.background[100],
8 | flex: 1,
9 | }));
10 |
11 | export interface StoryScreenProps {
12 | children?: React.ReactNode;
13 | }
14 |
15 | const behavior = Platform.OS === "ios" ? "padding" : undefined;
16 |
17 | export const StoryScreen = (props: StoryScreenProps) => (
18 |
19 | {props.children}
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/storybook/views/story.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { ScrollView, View, ViewStyle } from "react-native";
3 |
4 | export interface StoryProps {
5 | children?: React.ReactNode;
6 | }
7 |
8 | const ROOT: ViewStyle = { flex: 1 };
9 |
10 | export function Story(props: StoryProps) {
11 | return (
12 |
13 |
14 | {props.children}
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/storybook/views/use-case.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/native";
2 | import * as React from "react";
3 | import { ViewStyle } from "react-native";
4 | import { spacing, typography } from "../../app/theme";
5 |
6 | const Container = styled.View(props => ({
7 | backgroundColor: props.theme.background[100],
8 | }));
9 |
10 | const Header = styled.View(() => ({
11 | paddingTop: spacing[5],
12 | paddingHorizontal: spacing[5],
13 | }));
14 |
15 | const Title = styled.Text(props => ({
16 | fontFamily: typography.primary.bold,
17 | fontSize: spacing[5],
18 | fontWeight: "600",
19 | color: props.theme.text[100],
20 | }));
21 |
22 | const Description = styled.Text(props => ({
23 | paddingVertical: spacing[2],
24 | fontSize: spacing[4],
25 | fontWeight: "600",
26 | color: props.theme.text[200],
27 | }));
28 |
29 | const Component = styled.View((props: any) => ({
30 | padding: props.noPad ? 0 : spacing[4],
31 | backgroundColor: props.noBackground
32 | ? props.theme.transparent
33 | : props.theme.background[100],
34 | }));
35 |
36 | export interface UseCaseProps {
37 | /** The title. */
38 | title: string;
39 | /** When should we be using this? */
40 | description?: string;
41 | /** The component use case. */
42 | children: React.ReactNode;
43 | /** A style override. Rarely used. */
44 | style?: ViewStyle;
45 | /** Don't use any padding because it's important to see the spacing. */
46 | noPad?: boolean;
47 | /** Don't use background color because it's important to see the color. */
48 | noBackground?: boolean;
49 | }
50 |
51 | export function UseCase(props: UseCaseProps) {
52 | return (
53 |
54 |
55 | {props.title}
56 | {props.description ? (
57 | {props.description}
58 | ) : null}
59 |
60 |
61 | {props.children}
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-native",
4 | "target": "esnext",
5 | "lib": [
6 | "esnext"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "noEmit": true,
11 | "allowSyntheticDefaultImports": true,
12 | "resolveJsonModule": true,
13 | "esModuleInterop": true,
14 | "moduleResolution": "node",
15 | "noImplicitAny": true
16 | },
17 | "extends": "expo/tsconfig.base"
18 | }
19 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const createExpoWebpackConfigAsync = require("@expo/webpack-config")
2 |
3 | // Expo CLI will await this method so you can optionally return a promise.
4 | module.exports = async function (env, argv) {
5 | const config = await createExpoWebpackConfigAsync(env, argv)
6 | // If you want to add a new alias to the config.
7 | // config.resolve.alias["moduleA"] = "moduleB"
8 |
9 | // Maybe you want to turn off compression in dev mode.
10 | if (config.mode === "development") {
11 | config.devServer.compress = false
12 | }
13 |
14 | // Or prevent minimizing the bundle when you build.
15 | // if (config.mode === "production") {
16 | // config.optimization.minimize = false
17 | // }
18 |
19 | // Finally return the new config for the CLI to use.
20 | return config
21 | }
22 |
--------------------------------------------------------------------------------