├── .gitignore
├── .watchmanconfig
├── App.js
├── app.json
├── assets
├── icon.png
└── splash.png
├── babel.config.js
├── data
├── mutations.js
└── queries.js
├── package.json
├── src
├── AddTodo.js
├── Auth.js
├── Main.js
├── TodoItem.js
└── TodoList.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/**/*
2 | .expo/*
3 | .expo-shared/*
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 | web-report/
13 | config.js
14 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { StyleSheet, Text, View } from "react-native";
3 | import * as SecureStore from "expo-secure-store";
4 | import Auth from "./src/Auth";
5 | import Main from "./src/Main";
6 | import { ID_TOKEN_KEY } from "./config";
7 |
8 | const App = () => {
9 | const [token, setToken] = useState(null);
10 | const [user, setUser] = useState(null);
11 |
12 | useEffect(() => {
13 | handleLogin();
14 | }, []);
15 |
16 | const handleLogin = (isNewUser = false) => {
17 | SecureStore.getItemAsync(ID_TOKEN_KEY).then(session => {
18 | if (session) {
19 | const sessionObj = JSON.parse(session);
20 | const { exp, token, id, name } = sessionObj;
21 |
22 | if (exp > Math.floor(new Date().getTime() / 1000)) {
23 | setToken(token);
24 | setUser({ id, name, isNewUser });
25 | } else {
26 | handleLogout();
27 | }
28 | }
29 | });
30 | };
31 |
32 | const handleLogout = () => {
33 | SecureStore.deleteItemAsync(ID_TOKEN_KEY);
34 | setToken(null);
35 | };
36 |
37 | return (
38 |
39 | {token && user && }
40 |
41 |
42 | );
43 | };
44 |
45 | const styles = StyleSheet.create({
46 | container: {
47 | flex: 1,
48 | backgroundColor: "#fff",
49 | alignItems: "center",
50 | justifyContent: "center"
51 | }
52 | });
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "todo",
4 | "slug": "todo",
5 | "privacy": "public",
6 | "sdkVersion": "35.0.0",
7 | "platforms": [
8 | "ios",
9 | "android",
10 | "web"
11 | ],
12 | "version": "1.0.0",
13 | "orientation": "portrait",
14 | "icon": "./assets/icon.png",
15 | "splash": {
16 | "image": "./assets/splash.png",
17 | "resizeMode": "contain",
18 | "backgroundColor": "#ffffff"
19 | },
20 | "updates": {
21 | "fallbackToCacheTimeout": 0
22 | },
23 | "assetBundlePatterns": [
24 | "**/*"
25 | ],
26 | "ios": {
27 | "supportsTablet": true
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sezgi/todo-react-native/e5ce1bb48a2765a739d9e8338354a2aa91a14175/assets/icon.png
--------------------------------------------------------------------------------
/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sezgi/todo-react-native/e5ce1bb48a2765a739d9e8338354a2aa91a14175/assets/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function(api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/data/mutations.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 |
3 | export const INSERT_USER = gql`
4 | mutation($id: String, $name: String) {
5 | insert_users(objects: { id: $id, name: $name }) {
6 | affected_rows
7 | }
8 | }
9 | `;
10 |
11 | export const INSERT_TODO = gql`
12 | mutation($text: String!) {
13 | insert_todos(objects: { text: $text }) {
14 | returning {
15 | id
16 | text
17 | is_completed
18 | }
19 | }
20 | }
21 | `;
22 |
23 | export const UPDATE_TODO = gql`
24 | mutation($id: Int, $isCompleted: Boolean) {
25 | update_todos(
26 | where: { id: { _eq: $id } }
27 | _set: { is_completed: $isCompleted }
28 | ) {
29 | returning {
30 | id
31 | is_completed
32 | }
33 | }
34 | }
35 | `;
36 |
37 | export const DELETE_TODO = gql`
38 | mutation($id: Int) {
39 | delete_todos(where: { id: { _eq: $id } }) {
40 | returning {
41 | id
42 | }
43 | }
44 | }
45 | `;
46 |
--------------------------------------------------------------------------------
/data/queries.js:
--------------------------------------------------------------------------------
1 | import gql from "graphql-tag";
2 |
3 | export const GET_TODOS = gql`
4 | {
5 | todos(order_by: { created_at: desc }) {
6 | id
7 | text
8 | is_completed
9 | }
10 | }
11 | `;
12 |
--------------------------------------------------------------------------------
/package.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 | },
10 | "dependencies": {
11 | "@apollo/react-hooks": "^3.1.2",
12 | "apollo-boost": "^0.4.4",
13 | "expo": "^35.0.0",
14 | "expo-random": "~7.0.0",
15 | "expo-secure-store": "~7.0.0",
16 | "graphql": "^14.5.8",
17 | "jwt-decode": "^2.2.0",
18 | "prop-types": "^15.7.2",
19 | "query-string": "^6.8.3",
20 | "react": "16.8.3",
21 | "react-dom": "16.8.3",
22 | "react-native": "https://github.com/expo/react-native/archive/sdk-35.0.0.tar.gz",
23 | "react-native-web": "^0.11.7"
24 | },
25 | "devDependencies": {
26 | "babel-preset-expo": "^7.0.0"
27 | },
28 | "private": true
29 | }
30 |
--------------------------------------------------------------------------------
/src/AddTodo.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useMutation } from "@apollo/react-hooks";
3 | import {
4 | StyleSheet,
5 | Text,
6 | TextInput,
7 | TouchableOpacity,
8 | View
9 | } from "react-native";
10 | import { INSERT_TODO } from "../data/mutations";
11 | import { GET_TODOS } from "../data/queries";
12 |
13 | const AddTodo = () => {
14 | const [text, setText] = useState("");
15 | const [insertTodo, { loading, error }] = useMutation(INSERT_TODO);
16 |
17 | if (error) return `Error! ${error.message}`;
18 |
19 | return (
20 |
21 | setText(text)}
24 | value={text}
25 | />
26 | {
29 | insertTodo({
30 | variables: { text },
31 | refetchQueries: [{ query: GET_TODOS }]
32 | });
33 | setText("");
34 | }}
35 | disabled={loading || text === ""}
36 | >
37 | Add
38 |
39 |
40 | );
41 | };
42 |
43 | const styles = StyleSheet.create({
44 | container: {
45 | flexDirection: "row",
46 | alignItems: "center",
47 | marginBottom: 20
48 | },
49 | input: {
50 | flex: 1,
51 | borderWidth: 1,
52 | borderColor: "black",
53 | margin: 10,
54 | marginLeft: 0,
55 | padding: 10,
56 | fontSize: 24
57 | },
58 | button: {
59 | backgroundColor: "blue",
60 | padding: 13
61 | },
62 | buttonText: {
63 | color: "white",
64 | fontSize: 20
65 | }
66 | });
67 |
68 | export default AddTodo;
69 |
--------------------------------------------------------------------------------
/src/Auth.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Button, Alert } from "react-native";
4 | import { AuthSession } from "expo";
5 | import * as Random from "expo-random";
6 | import * as SecureStore from "expo-secure-store";
7 | import jwtDecoder from "jwt-decode";
8 | import queryString from "query-string";
9 | import {
10 | AUTH_CLIENT_ID,
11 | AUTH_DOMAIN,
12 | AUTH_NAMESPACE,
13 | ID_TOKEN_KEY,
14 | NONCE_KEY
15 | } from "../config";
16 |
17 | const generateNonce = async () => {
18 | const nonce = String.fromCharCode.apply(
19 | null,
20 | await Random.getRandomBytesAsync(16)
21 | );
22 | await SecureStore.setItemAsync(NONCE_KEY, nonce);
23 | return nonce;
24 | };
25 |
26 | const Auth = ({ token, onLogin, onLogout }) => {
27 | const handleLoginPress = async () => {
28 | const nonce = await generateNonce();
29 | AuthSession.startAsync({
30 | authUrl:
31 | `${AUTH_DOMAIN}/authorize?` +
32 | queryString.stringify({
33 | client_id: AUTH_CLIENT_ID,
34 | response_type: "id_token",
35 | scope: "openid profile email",
36 | redirect_uri: AuthSession.getRedirectUrl(),
37 | nonce
38 | })
39 | }).then(result => {
40 | if (result.type === "success") {
41 | decodeToken(result.params.id_token);
42 | } else if (result.params && result.params.error) {
43 | Alert.alert(
44 | "Error",
45 | result.params.error_description ||
46 | "Something went wrong while logging in."
47 | );
48 | }
49 | });
50 | };
51 |
52 | const decodeToken = token => {
53 | const decodedToken = jwtDecoder(token);
54 | const { nonce, sub, name, exp } = decodedToken;
55 |
56 | SecureStore.getItemAsync(NONCE_KEY).then(storedNonce => {
57 | if (nonce == storedNonce) {
58 | SecureStore.setItemAsync(
59 | ID_TOKEN_KEY,
60 | JSON.stringify({
61 | id: sub,
62 | name,
63 | exp,
64 | token
65 | })
66 | ).then(() => onLogin(decodedToken[AUTH_NAMESPACE].isNewUser));
67 | } else {
68 | Alert.alert("Error", "Nonces don't match");
69 | return;
70 | }
71 | });
72 | };
73 |
74 | return token ? (
75 |
76 | ) : (
77 |
78 | );
79 | };
80 |
81 | Auth.propTypes = {
82 | token: PropTypes.string,
83 | onLogin: PropTypes.func,
84 | onLogout: PropTypes.func
85 | };
86 |
87 | export default Auth;
88 |
--------------------------------------------------------------------------------
/src/Main.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import PropTypes from "prop-types";
3 | import { ApolloProvider } from "@apollo/react-hooks";
4 | import ApolloClient from "apollo-boost";
5 | import { StyleSheet, Text, View, ActivityIndicator } from "react-native";
6 | import { GRAPHQL_ENDPOINT } from "../config";
7 | import { INSERT_USER } from "../data/mutations";
8 | import TodoList from "./TodoList";
9 | import AddTodo from "./AddTodo";
10 |
11 | const Main = ({ token, user }) => {
12 | const [client, setClient] = useState(null);
13 |
14 | useEffect(() => {
15 | const { id, name, isNewUser } = user;
16 |
17 | const client = new ApolloClient({
18 | uri: GRAPHQL_ENDPOINT,
19 | headers: {
20 | Authorization: `Bearer ${token}`
21 | }
22 | });
23 |
24 | if (isNewUser) {
25 | client.mutate({
26 | mutation: INSERT_USER,
27 | variables: { id, name }
28 | });
29 | }
30 |
31 | setClient(client);
32 | }, []);
33 |
34 | if (!client) {
35 | return ;
36 | }
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | Main.propTypes = {
49 | token: PropTypes.string.isRequired,
50 | user: PropTypes.object.isRequired
51 | };
52 |
53 | export default Main;
54 |
--------------------------------------------------------------------------------
/src/TodoItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { useMutation } from "@apollo/react-hooks";
4 | import { StyleSheet, Text, View, TouchableOpacity } from "react-native";
5 | import { DELETE_TODO } from "../data/mutations";
6 | import { UPDATE_TODO } from "../data/mutations";
7 | import { GET_TODOS } from "../data/queries";
8 |
9 | const TodoItem = ({ item }) => {
10 | const { id, text, is_completed } = item;
11 | const [
12 | deleteTodo,
13 | { loading: deleteLoading, error: deleteError }
14 | ] = useMutation(DELETE_TODO);
15 | const [
16 | updateTodo,
17 | { loading: updateLoading, error: updateError }
18 | ] = useMutation(UPDATE_TODO);
19 |
20 | if (deleteError || updateError) return `Error! ${error.message}`;
21 |
22 | return (
23 |
24 | {
27 | if (!updateLoading) {
28 | updateTodo({
29 | variables: { id, isCompleted: !is_completed }
30 | });
31 | }
32 | }}
33 | >
34 | {is_completed ? "☑" : "☒"}
35 |
36 |
37 | {text}
38 |
39 | {
42 | deleteTodo({
43 | variables: { id },
44 | refetchQueries: [{ query: GET_TODOS }]
45 | });
46 | }}
47 | disabled={deleteLoading}
48 | >
49 | Delete
50 |
51 |
52 | );
53 | };
54 |
55 | TodoItem.propTypes = {
56 | item: PropTypes.object.isRequired
57 | };
58 |
59 | const styles = StyleSheet.create({
60 | container: {
61 | flexDirection: "row",
62 | alignItems: "center"
63 | },
64 | mark: {
65 | fontSize: 30
66 | },
67 | item: {
68 | padding: 10,
69 | fontSize: 24
70 | },
71 | button: {
72 | backgroundColor: "green",
73 | padding: 5,
74 | marginLeft: "auto"
75 | },
76 | buttonText: {
77 | color: "white",
78 | fontSize: 14
79 | },
80 | completed: {
81 | color: "lightgray"
82 | }
83 | });
84 |
85 | export default TodoItem;
86 |
--------------------------------------------------------------------------------
/src/TodoList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "@apollo/react-hooks";
3 | import {
4 | FlatList,
5 | StyleSheet,
6 | Text,
7 | View,
8 | ActivityIndicator
9 | } from "react-native";
10 | import TodoItem from "./TodoItem";
11 | import { GET_TODOS } from "../data/queries";
12 |
13 | const TodoList = () => {
14 | const { loading, error, data } = useQuery(GET_TODOS);
15 |
16 | if (error) return `Error! ${error.message}`;
17 |
18 | return (
19 |
20 | {loading ? (
21 |
22 | ) : (
23 | }
26 | keyExtractor={item => item.id.toString()}
27 | />
28 | )}
29 |
30 | );
31 | };
32 |
33 | const styles = StyleSheet.create({
34 | container: {
35 | width: 300,
36 | height: 500
37 | }
38 | });
39 |
40 | export default TodoList;
41 |
--------------------------------------------------------------------------------