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