├── assets ├── icon.png ├── logo.jpg ├── splash.png ├── favicon.png └── adaptive-icon.png ├── babel.config.js ├── tailwind.js ├── .gitignore ├── tailwind.config.js ├── App.js ├── README.md ├── src ├── Layout.js ├── Header.js ├── util │ └── api.js ├── Router.js ├── AuthProvider.js └── stacks │ ├── AppStack.js │ └── AuthStack.js ├── app.json ├── package.json └── styles.json /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggelashvili/laravel-fortify-mobile-app/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggelashvili/laravel-fortify-mobile-app/HEAD/assets/logo.jpg -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggelashvili/laravel-fortify-mobile-app/HEAD/assets/splash.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggelashvili/laravel-fortify-mobile-app/HEAD/assets/favicon.png -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ggelashvili/laravel-fortify-mobile-app/HEAD/assets/adaptive-icon.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /tailwind.js: -------------------------------------------------------------------------------- 1 | import {create} from 'tailwind-rn'; 2 | import styles from './styles.json'; 3 | 4 | const {tailwind, getColor} = create(styles); 5 | 6 | export {tailwind, getColor}; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | .expo 12 | .expo-shared 13 | 14 | # macOS 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | // removeDeprecatedGapUtilities: true, 4 | // purgeLayersByDefault: true, 5 | }, 6 | purge: [], 7 | theme: { 8 | extend: {}, 9 | }, 10 | variants: {}, 11 | plugins: [], 12 | } 13 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AuthProvider} from './src/AuthProvider' 3 | import Router from './src/Router' 4 | 5 | const App = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default App 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Front-END - Laravel Fortify With Mobile App 2 | 3 | Authenticating Mobile Application (React Native) Using Laravel Fortify & Sanctum (Token Based Auth) - https://youtu.be/ymLpLWklzxQ 4 | 5 | See https://github.com/ggelashvili/laravel-fortify-spa-backend for Laravel API back-end. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Header from './Header' 3 | import {View} from 'react-native' 4 | 5 | const Layout = ({children, title}) => ( 6 | 7 |
8 | {children} 9 | 10 | ) 11 | 12 | export default Layout 13 | -------------------------------------------------------------------------------- /src/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Image, Text} from 'react-native' 3 | import {tailwind} from '../tailwind' 4 | 5 | const Header = ({title}) => { 6 | return ( 7 | <> 8 | 12 | {title} 13 | 14 | ) 15 | } 16 | 17 | export default Header 18 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "laravel-fortify-auth-mobile-app-frontend", 4 | "slug": "laravel-fortify-auth-mobile-app-frontend", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/util/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as SecureStore from 'expo-secure-store' 3 | 4 | const api = ({token = null} = {}) => { 5 | const api = axios.create({ 6 | baseURL: 'http://10.0.2.2:5000/api/', 7 | }) 8 | 9 | if (token) { 10 | api.defaults.headers.common['Authorization'] = `Bearer ${token}` 11 | } 12 | 13 | api.interceptors.response.use(response => response, error => { 14 | if (error.response.status === 401) { 15 | SecureStore.deleteItemAsync('user') 16 | 17 | return Promise.reject({status: 401, errors: ['Unauthorized']}) 18 | } 19 | 20 | if (error.response?.status === 422) { 21 | let errors = Object.values(error?.response?.data?.errors || {}) 22 | 23 | return Promise.reject({status: 422, errorsRaw: errors, errors: errors.reduce(error => error)}) 24 | } 25 | 26 | console.error(error) 27 | 28 | return Promise.reject({status: error.response?.status, errors: ['Oops!']}) 29 | }) 30 | 31 | return api 32 | } 33 | 34 | export default api 35 | -------------------------------------------------------------------------------- /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 | "@react-navigation/native": "^5.8.2", 12 | "@react-navigation/stack": "^5.11.1", 13 | "axios": "^0.20.0", 14 | "expo": "~39.0.2", 15 | "expo-secure-store": "~9.2.0", 16 | "expo-status-bar": "~1.0.2", 17 | "react": "^17.0.1", 18 | "react-dom": "^17.0.1", 19 | "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.4.tar.gz", 20 | "react-native-web": "~0.13.12", 21 | "sass": "^1.28.0", 22 | "tailwind-rn": "^2.0.1", 23 | "react-native-gesture-handler": "~1.7.0", 24 | "react-native-reanimated": "~1.13.0", 25 | "react-native-screens": "~2.10.1", 26 | "react-native-safe-area-context": "3.1.4", 27 | "@react-native-community/masked-view": "0.1.10" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "~7.9.0" 31 | }, 32 | "private": true 33 | } 34 | -------------------------------------------------------------------------------- /src/Router.js: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect, useContext} from "react" 2 | import {View, ActivityIndicator} from 'react-native' 3 | import {NavigationContainer} from '@react-navigation/native' 4 | import {AuthContext} from './AuthProvider' 5 | import * as SecureStore from 'expo-secure-store' 6 | import AuthStack from './stacks/AuthStack' 7 | import AppStack from './stacks/AppStack' 8 | 9 | const Router = () => { 10 | const {user, setUser} = useContext(AuthContext) 11 | const [loading, setLoading] = useState(true) 12 | 13 | useEffect(() => { 14 | SecureStore.getItemAsync('user').then(userString => { 15 | setUser(JSON.parse(userString)) 16 | 17 | setLoading(false) 18 | }).catch(err => { 19 | console.log(err) 20 | }) 21 | 22 | }, []) 23 | 24 | if (loading) { 25 | return ( 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | return ( 33 | 34 | {user ? : } 35 | 36 | ) 37 | } 38 | 39 | export default Router 40 | -------------------------------------------------------------------------------- /src/AuthProvider.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react' 2 | import * as SecureStore from 'expo-secure-store' 3 | import api from './util/api' 4 | 5 | export const AuthContext = React.createContext({}) 6 | 7 | export const AuthProvider = ({children}) => { 8 | const [user, setUser] = useState(null) 9 | const [error, setError] = useState(null) 10 | 11 | useEffect(() => { 12 | if (error) { 13 | const timer = setTimeout(() => { 14 | setError(null) 15 | }, 2500) 16 | 17 | return () => clearTimeout(timer) 18 | } 19 | }, [error]) 20 | 21 | const logIn = (email, password) => { 22 | api().post('/auth/token', { 23 | email, 24 | password, 25 | device_name: 'mobile', 26 | }).then(response => { 27 | const userResponse = { 28 | email: response.data.user.email, 29 | name: response.data.user.name, 30 | token: response.data.token, 31 | } 32 | 33 | setUser(userResponse) 34 | setError(null) 35 | 36 | SecureStore.setItemAsync('user', JSON.stringify(userResponse)) 37 | }).catch(({errors}) => { 38 | setError(errors[0]) 39 | }) 40 | } 41 | 42 | const logOut = () => { 43 | api({token: user.token}).delete('/auth/token').then(() => { 44 | setUser(null) 45 | 46 | SecureStore.deleteItemAsync('user') 47 | }).catch(({errors}) => { 48 | setError(errors[0]) 49 | }) 50 | } 51 | 52 | return ( 53 | 54 | {children} 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/stacks/AppStack.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState, useEffect} from 'react' 2 | import {createStackNavigator} from '@react-navigation/stack' 3 | import {AuthContext} from '../AuthProvider' 4 | import {Button, Text, View} from 'react-native' 5 | import api from '../util/api' 6 | import {tailwind} from '../../tailwind' 7 | import Layout from '../Layout' 8 | 9 | const Stack = createStackNavigator() 10 | 11 | const HomeScreen = ({navigation}) => { 12 | const {user, logOut} = useContext(AuthContext) 13 | const [tickets, setTickets] = useState(null) 14 | 15 | useEffect(() => { 16 | api({token: user.token}).get('/tickets').then(({data}) => { 17 | setTickets(data) 18 | }).catch(error => { 19 | console.log(error) 20 | }) 21 | }, []) 22 | 23 | return ( 24 | 25 | {tickets !== null 26 | ? ( 27 | <> 28 | Number of Tickets: {tickets.length} 29 | First Ticket ID: {tickets[0].id} 30 | 31 | ) : null} 32 | 33 |