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