├── .gitignore ├── README.md ├── app.json ├── app ├── (auth) │ ├── _layout.tsx │ ├── home.tsx │ └── profile.tsx ├── (public) │ ├── _layout.tsx │ ├── login.tsx │ ├── register.tsx │ └── reset.tsx ├── _layout.tsx └── index.tsx ├── babel.config.js ├── banner.png ├── index.js ├── package-lock.json ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Authentication with Clerk and Expo Router 2 | 3 | Check out the full [video tutorial](https://www.youtube.com/watch?v=zh6Sc1flK2g) and also [the written tutorial](https://galaxies.dev/react-native-authentication-clerk)! 4 | 5 | Use [Clerk](https://clerk.com/?utm_source=sponsorship&utm_medium=video&utm_campaign=simong&utm_content=08_15_2023) for epic user management 🔥 6 | 7 | ## 🚀 How to use 8 | 9 | ```sh 10 | npx expo 11 | ``` 12 | 13 | ## 📝 Notes 14 | 15 | - [Expo Router: Docs](https://expo.github.io/router) 16 | - [Expo Router: Repo](https://github.com/expo/router) 17 | 18 | 19 | ## 🚀 More 20 | 21 | **Take a shortcut from web developer to mobile development fluency with guided learning** 22 | 23 | Enjoyed this project? Learn to use React Native to build production-ready, native mobile apps for both iOS and Android based on your existing web development skills. 24 | 25 | 26 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "scheme": "acme", 4 | "web": { 5 | "bundler": "metro" 6 | }, 7 | "name": "clerkApp", 8 | "slug": "clerkApp", 9 | "plugins": [ 10 | "expo-router" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/(auth)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from 'expo-router'; 2 | import { Ionicons } from '@expo/vector-icons'; 3 | import { Pressable } from 'react-native'; 4 | import { useAuth } from '@clerk/clerk-expo'; 5 | 6 | export const LogoutButton = () => { 7 | const { signOut } = useAuth(); 8 | 9 | const doLogout = () => { 10 | signOut(); 11 | }; 12 | 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const TabsPage = () => { 21 | const { isSignedIn } = useAuth(); 22 | 23 | return ( 24 | 31 | , 36 | tabBarLabel: 'Home', 37 | }} 38 | redirect={!isSignedIn} 39 | /> 40 | , 45 | tabBarLabel: 'My Profile', 46 | headerRight: () => , 47 | }} 48 | redirect={!isSignedIn} 49 | /> 50 | 51 | ); 52 | }; 53 | 54 | export default TabsPage; 55 | -------------------------------------------------------------------------------- /app/(auth)/home.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text } from 'react-native'; 2 | import React from 'react'; 3 | import { useUser } from '@clerk/clerk-expo'; 4 | 5 | const Home = () => { 6 | const { user } = useUser(); 7 | 8 | return ( 9 | 10 | Welcome, {user?.emailAddresses[0].emailAddress} 🎉 11 | 12 | ); 13 | }; 14 | 15 | export default Home; 16 | -------------------------------------------------------------------------------- /app/(auth)/profile.tsx: -------------------------------------------------------------------------------- 1 | import { View, Text, Button, TextInput, StyleSheet } from 'react-native'; 2 | import { useState } from 'react'; 3 | import { useUser } from '@clerk/clerk-expo'; 4 | 5 | const Profile = () => { 6 | const { user } = useUser(); 7 | const [firstName, setFirstName] = useState(user.firstName); 8 | const [lastName, setLastName] = useState(user.lastName); 9 | 10 | const onSaveUser = async () => { 11 | try { 12 | // This is not working! 13 | const result = await user.update({ 14 | firstName: 'John', 15 | lastName: 'Doe', 16 | }); 17 | console.log('🚀 ~ file: profile.tsx:16 ~ onSaveUser ~ result:', result); 18 | } catch (e) { 19 | console.log('🚀 ~ file: profile.tsx:18 ~ onSaveUser ~ e', JSON.stringify(e)); 20 | } 21 | }; 22 | 23 | return ( 24 | 25 | 26 | Good morning {user.firstName} {user.lastName}! 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | const styles = StyleSheet.create({ 37 | container: { 38 | flex: 1, 39 | justifyContent: 'center', 40 | padding: 40, 41 | }, 42 | inputField: { 43 | marginVertical: 4, 44 | height: 50, 45 | borderWidth: 1, 46 | borderColor: '#6c47ff', 47 | borderRadius: 4, 48 | padding: 10, 49 | backgroundColor: '#fff', 50 | }, 51 | }); 52 | 53 | export default Profile; 54 | -------------------------------------------------------------------------------- /app/(public)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stack } from 'expo-router'; 3 | 4 | const PublicLayout = () => { 5 | return ( 6 | 14 | 19 | 24 | 29 | 30 | ); 31 | }; 32 | 33 | export default PublicLayout; 34 | -------------------------------------------------------------------------------- /app/(public)/login.tsx: -------------------------------------------------------------------------------- 1 | import { useSignIn } from '@clerk/clerk-expo'; 2 | import { Link } from 'expo-router'; 3 | import React, { useState } from 'react'; 4 | import { View, StyleSheet, TextInput, Button, Pressable, Text, Alert } from 'react-native'; 5 | import Spinner from 'react-native-loading-spinner-overlay'; 6 | 7 | const Login = () => { 8 | const { signIn, setActive, isLoaded } = useSignIn(); 9 | 10 | const [emailAddress, setEmailAddress] = useState(''); 11 | const [password, setPassword] = useState(''); 12 | const [loading, setLoading] = useState(false); 13 | 14 | const onSignInPress = async () => { 15 | if (!isLoaded) { 16 | return; 17 | } 18 | setLoading(true); 19 | try { 20 | const completeSignIn = await signIn.create({ 21 | identifier: emailAddress, 22 | password, 23 | }); 24 | 25 | // This indicates the user is signed in 26 | await setActive({ session: completeSignIn.createdSessionId }); 27 | } catch (err: any) { 28 | alert(err.errors[0].message); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | return ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Forgot password? 46 | 47 | 48 | 49 | 50 | Create Account 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | const styles = StyleSheet.create({ 58 | container: { 59 | flex: 1, 60 | justifyContent: 'center', 61 | padding: 20, 62 | }, 63 | inputField: { 64 | marginVertical: 4, 65 | height: 50, 66 | borderWidth: 1, 67 | borderColor: '#6c47ff', 68 | borderRadius: 4, 69 | padding: 10, 70 | backgroundColor: '#fff', 71 | }, 72 | button: { 73 | margin: 8, 74 | alignItems: 'center', 75 | }, 76 | }); 77 | 78 | export default Login; 79 | -------------------------------------------------------------------------------- /app/(public)/register.tsx: -------------------------------------------------------------------------------- 1 | import { Button, TextInput, View, StyleSheet } from 'react-native'; 2 | import { useSignUp } from '@clerk/clerk-expo'; 3 | import Spinner from 'react-native-loading-spinner-overlay'; 4 | import { useState } from 'react'; 5 | import { Stack } from 'expo-router'; 6 | 7 | const Register = () => { 8 | const { isLoaded, signUp, setActive } = useSignUp(); 9 | 10 | const [emailAddress, setEmailAddress] = useState(''); 11 | const [password, setPassword] = useState(''); 12 | const [pendingVerification, setPendingVerification] = useState(false); 13 | const [code, setCode] = useState(''); 14 | const [loading, setLoading] = useState(false); 15 | 16 | // Create the user and send the verification email 17 | const onSignUpPress = async () => { 18 | if (!isLoaded) { 19 | return; 20 | } 21 | setLoading(true); 22 | 23 | try { 24 | // Create the user on Clerk 25 | await signUp.create({ 26 | emailAddress, 27 | password, 28 | }); 29 | 30 | // Send verification Email 31 | await signUp.prepareEmailAddressVerification({ strategy: 'email_code' }); 32 | 33 | // change the UI to verify the email address 34 | setPendingVerification(true); 35 | } catch (err: any) { 36 | alert(err.errors[0].message); 37 | } finally { 38 | setLoading(false); 39 | } 40 | }; 41 | 42 | // Verify the email address 43 | const onPressVerify = async () => { 44 | if (!isLoaded) { 45 | return; 46 | } 47 | setLoading(true); 48 | 49 | try { 50 | const completeSignUp = await signUp.attemptEmailAddressVerification({ 51 | code, 52 | }); 53 | 54 | await setActive({ session: completeSignUp.createdSessionId }); 55 | } catch (err: any) { 56 | alert(err.errors[0].message); 57 | } finally { 58 | setLoading(false); 59 | } 60 | }; 61 | 62 | return ( 63 | 64 | 65 | 66 | 67 | {!pendingVerification && ( 68 | <> 69 | 70 | 71 | 72 | 73 | 74 | )} 75 | 76 | {pendingVerification && ( 77 | <> 78 | 79 | 80 | 81 | 82 | 83 | )} 84 | 85 | ); 86 | }; 87 | 88 | const styles = StyleSheet.create({ 89 | container: { 90 | flex: 1, 91 | justifyContent: 'center', 92 | padding: 20, 93 | }, 94 | inputField: { 95 | marginVertical: 4, 96 | height: 50, 97 | borderWidth: 1, 98 | borderColor: '#6c47ff', 99 | borderRadius: 4, 100 | padding: 10, 101 | backgroundColor: '#fff', 102 | }, 103 | button: { 104 | margin: 8, 105 | alignItems: 'center', 106 | }, 107 | }); 108 | 109 | export default Register; 110 | -------------------------------------------------------------------------------- /app/(public)/reset.tsx: -------------------------------------------------------------------------------- 1 | import { View, StyleSheet, TextInput, Button } from 'react-native'; 2 | import React, { useState } from 'react'; 3 | import { Stack } from 'expo-router'; 4 | import { useSignIn } from '@clerk/clerk-expo'; 5 | 6 | const PwReset = () => { 7 | const [emailAddress, setEmailAddress] = useState(''); 8 | const [password, setPassword] = useState(''); 9 | const [code, setCode] = useState(''); 10 | const [successfulCreation, setSuccessfulCreation] = useState(false); 11 | const { signIn, setActive } = useSignIn(); 12 | 13 | // Request a passowrd reset code by email 14 | const onRequestReset = async () => { 15 | try { 16 | await signIn.create({ 17 | strategy: 'reset_password_email_code', 18 | identifier: emailAddress, 19 | }); 20 | setSuccessfulCreation(true); 21 | } catch (err: any) { 22 | alert(err.errors[0].message); 23 | } 24 | }; 25 | 26 | // Reset the password with the code and the new password 27 | const onReset = async () => { 28 | try { 29 | const result = await signIn.attemptFirstFactor({ 30 | strategy: 'reset_password_email_code', 31 | code, 32 | password, 33 | }); 34 | console.log(result); 35 | alert('Password reset successfully'); 36 | 37 | // Set the user session active, which will log in the user automatically 38 | await setActive({ session: result.createdSessionId }); 39 | } catch (err: any) { 40 | alert(err.errors[0].message); 41 | } 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 48 | {!successfulCreation && ( 49 | <> 50 | 51 | 52 | 53 | 54 | )} 55 | 56 | {successfulCreation && ( 57 | <> 58 | 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | 66 | ); 67 | }; 68 | 69 | const styles = StyleSheet.create({ 70 | container: { 71 | flex: 1, 72 | justifyContent: 'center', 73 | padding: 20, 74 | }, 75 | inputField: { 76 | marginVertical: 4, 77 | height: 50, 78 | borderWidth: 1, 79 | borderColor: '#6c47ff', 80 | borderRadius: 4, 81 | padding: 10, 82 | backgroundColor: '#fff', 83 | }, 84 | button: { 85 | margin: 8, 86 | alignItems: 'center', 87 | }, 88 | }); 89 | 90 | export default PwReset; 91 | -------------------------------------------------------------------------------- /app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { ClerkProvider, useAuth } from '@clerk/clerk-expo'; 2 | import { Slot, useRouter, useSegments } from 'expo-router'; 3 | import { useEffect } from 'react'; 4 | import * as SecureStore from 'expo-secure-store'; 5 | 6 | const CLERK_PUBLISHABLE_KEY = 'pk_test_dG91Y2hpbmctYmVkYnVnLTQ0LmNsZXJrLmFjY291bnRzLmRldiQ'; 7 | 8 | const InitialLayout = () => { 9 | const { isLoaded, isSignedIn } = useAuth(); 10 | const segments = useSegments(); 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | if (!isLoaded) return; 15 | 16 | const inTabsGroup = segments[0] === '(auth)'; 17 | 18 | console.log('User changed: ', isSignedIn); 19 | 20 | if (isSignedIn && !inTabsGroup) { 21 | router.replace('/home'); 22 | } else if (!isSignedIn) { 23 | router.replace('/login'); 24 | } 25 | }, [isSignedIn]); 26 | 27 | return ; 28 | }; 29 | 30 | const tokenCache = { 31 | async getToken(key: string) { 32 | try { 33 | return SecureStore.getItemAsync(key); 34 | } catch (err) { 35 | return null; 36 | } 37 | }, 38 | async saveToken(key: string, value: string) { 39 | try { 40 | return SecureStore.setItemAsync(key, value); 41 | } catch (err) { 42 | return; 43 | } 44 | }, 45 | }; 46 | 47 | const RootLayout = () => { 48 | return ( 49 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default RootLayout; 56 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import { ActivityIndicator, View } from 'react-native'; 2 | 3 | const StartPage = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | 11 | export default StartPage; 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | "@babel/plugin-proposal-export-namespace-from", 7 | "react-native-reanimated/plugin", 8 | require.resolve("expo-router/babel"), 9 | ], 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Galaxies-dev/react-native-clerk-auth/b52cf0c1ea4b181513918a2591633f9a10fd210e/banner.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import "expo-router/entry"; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "expo start", 4 | "android": "expo start --android", 5 | "ios": "expo start --ios", 6 | "web": "expo start --web" 7 | }, 8 | "dependencies": { 9 | "@clerk/clerk-expo": "^0.18.10", 10 | "expo": "^49.0.0-beta", 11 | "expo-constants": "~14.4.2", 12 | "expo-linking": "~5.0.2", 13 | "expo-router": "2.0.0-rc.10", 14 | "expo-splash-screen": "~0.20.3", 15 | "expo-status-bar": "~1.6.0", 16 | "react": "18.2.0", 17 | "react-dom": "18.2.0", 18 | "react-native": "0.72.0", 19 | "react-native-gesture-handler": "~2.12.0", 20 | "react-native-loading-spinner-overlay": "^3.0.1", 21 | "react-native-reanimated": "~3.3.0", 22 | "react-native-safe-area-context": "4.6.3", 23 | "react-native-screens": "~3.22.0", 24 | "react-native-web": "~0.19.6", 25 | "expo-secure-store": "~12.3.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.19.3", 29 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9", 30 | "@types/react": "^18.2.14", 31 | "@types/react-native": "^0.72.2", 32 | "typescript": "^5.1.6" 33 | }, 34 | "resolutions": { 35 | "metro": "^0.73.7", 36 | "metro-resolver": "^0.73.7" 37 | }, 38 | "overrides": { 39 | "metro": "^0.73.7", 40 | "metro-resolver": "^0.73.7" 41 | }, 42 | "name": "clerkapp", 43 | "version": "1.0.0", 44 | "private": true 45 | } 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": {}, 3 | "extends": "expo/tsconfig.base" 4 | } 5 | --------------------------------------------------------------------------------