├── .gitignore ├── App.tsx ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── fonts │ └── SpaceMono-Regular.ttf ├── icon.png ├── images │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png └── splash.png ├── babel.config.js ├── cache.ts ├── components ├── OAuth.tsx └── Styles.ts ├── docs ├── clerk-logo-dark.png ├── clerk-logo-light.png ├── sign-in-oauth-demo.gif ├── sign-up-otp-demo.gif └── whitelist-redirect-urls-for-mobile-oauth.png ├── hooks ├── useCachedResources.ts └── useWarmUpBrowser.ts ├── index.js ├── logger.ts ├── navigation ├── LinkingConfiguration.ts └── index.tsx ├── package-lock.json ├── package.json ├── screens ├── MyProfileScreen.tsx ├── SignInScreen.tsx ├── SignUpScreen.tsx └── VerifyCodeScreen.tsx ├── tsconfig.json └── types.tsx /.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 | 16 | # @generated expo-cli sync-e7dcf75f4e856f7b6f3239b3f3a7dd614ee755a8 17 | # The following patterns were generated by expo-cli 18 | 19 | # OSX 20 | # 21 | .DS_Store 22 | 23 | # Xcode 24 | # 25 | build/ 26 | *.pbxuser 27 | !default.pbxuser 28 | *.mode1v3 29 | !default.mode1v3 30 | *.mode2v3 31 | !default.mode2v3 32 | *.perspectivev3 33 | !default.perspectivev3 34 | xcuserdata 35 | *.xccheckout 36 | *.moved-aside 37 | DerivedData 38 | *.hmap 39 | *.ipa 40 | *.xcuserstate 41 | project.xcworkspace 42 | 43 | # Android/IntelliJ 44 | # 45 | build/ 46 | .idea 47 | .gradle 48 | local.properties 49 | *.iml 50 | *.hprof 51 | 52 | # node.js 53 | # 54 | node_modules/ 55 | npm-debug.log 56 | yarn-error.log 57 | 58 | # BUCK 59 | buck-out/ 60 | \.buckd/ 61 | *.keystore 62 | !debug.keystore 63 | 64 | # Bundle artifacts 65 | *.jsbundle 66 | 67 | # CocoaPods 68 | /ios/Pods/ 69 | 70 | # Expo 71 | .expo/ 72 | web-build/ 73 | dist/ 74 | 75 | # @end expo-cli 76 | yarn.lock 77 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StatusBar } from "expo-status-bar"; 3 | import { SafeAreaProvider } from "react-native-safe-area-context"; 4 | import useCachedResources from "./hooks/useCachedResources"; 5 | import Navigation from "./navigation"; 6 | import { ClerkProvider } from "@clerk/clerk-expo"; 7 | import { tokenCache } from "./cache"; 8 | import * as SplashScreen from "expo-splash-screen"; 9 | 10 | // Your publishable Key goes here 11 | const publishableKey = "pk_XXXXXXXXXXXXXXXXXXX"; 12 | 13 | export default function App() { 14 | const isLoadingComplete = useCachedResources(); 15 | 16 | React.useEffect(() => { 17 | SplashScreen.preventAutoHideAsync(); 18 | }, []); 19 | 20 | if (!isLoadingComplete) { 21 | return null; 22 | } else { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

10 | 11 | # Clerk Expo Starter 12 | 13 | > If you want to use the new file-system based router that was introduced with [Expo Router V2](https://blog.expo.dev/introducing-expo-router-v2-3850fd5c3ca1), you can check out [this branch](https://github.com/clerkinc/clerk-expo-starter/tree/router-v2) 14 | 15 | This starter project shows how to use [Clerk](https://www.clerk.dev/?utm_source=github&utm_medium=starter_repos&utm_campaign=expo_starter) with [Expo](https://expo.dev/) to authenticate users in your [React Native](https://reactnative.dev/) application. When users sign up or sign in, Clerk handles the state of the authentication and switches between public or [protected routes](https://reactnavigation.org/docs/auth-flow). 16 | 17 | [![chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://discord.com/invite/b5rXHjAg7A) 18 | [![documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://docs.clerk.dev) 19 | [![twitter](https://img.shields.io/twitter/follow/ClerkDev?style=social)](https://twitter.com/intent/follow?screen_name=ClerkDev) 20 | 21 | --- 22 | 23 | **Clerk is Hiring!** 24 | 25 | Would you like to work on Open Source software and help maintain this repository? [Apply today!](https://apply.workable.com/clerk-dev/) 26 | 27 | --- 28 | 29 | ## Demo 30 | 31 | ### Sign up using One-time passcodes (OTP) & Sign in using OAuth 32 | 33 |
34 | 35 | 36 |
37 | 38 | ## Getting Started 39 | 40 | ### Prerequisites 41 | 42 | - React v16+ 43 | - Node.js v14+ 44 | 45 | ### Setup 46 | 47 | To run the example locally you need to: 48 | 49 | 1. Sign up at [Clerk.dev](https://www.clerk.dev/?utm_source=github&utm_medium=starter_repos&utm_campaign=expo_starter). 50 | 2. Go to your [Clerk dashboard](https://dashboard.clerk.dev/?utm_source=github&utm_medium=starter_repos&utm_campaign=expo_starter) and create an application. 51 | 3. Set your publishableKey in `App.tsx` or if you are using a legacy key frontendApi 52 | 4. `npm i` to install the required dependencies. 53 | 5. `npm run start` to launch the Expo development server. 54 | 55 | ### Sign up & Sign in configuration 56 | 57 | For the sign up flow to work as demonstrated, you need to log into your [Clerk Dashboard](https://dashboard.clerk.dev/?utm_source=github&utm_medium=starter_repos&utm_campaign=expo_starter) and make sure the following settings have been configured in **User & Authentication** and **Social login** sections: 58 | 59 | #### For development instances 60 | 61 | 1. In Contact information section enable **Email Address** and pick **Email verification code** method in the modal. 62 | 2. In Authentication factors section enable **Password** and **Email verification code**. 63 | 3. In Personal information, enable **Name** to use first and last names during sign up 64 | 4. In Social Login, enable **Google** Oauth provider. 65 | 66 | #### For production instances 67 | 68 | 1. Follow all the steps above 69 | 2. Whitelist your custom redirect URL via [Clerk Dashboard](https://dashboard.clerk.dev/last-active?path=/user-authentication/social-login) for maximum security during native OAuth flows. 70 | ![This is an image](./docs/whitelist-redirect-urls-for-mobile-oauth.png) 71 | 72 | ## Learn more 73 | 74 | To learn more about Clerk and Expo, take a look at our 75 | [official documentation](https://reference.clerk.dev/reference/clerk-expo?utm_source=github&utm_medium=starter_repos&utm_campaign=expo_starter). 76 | 77 | ## Support 78 | 79 | You can get in touch with us in any of the following ways: 80 | 81 | - Join our official community [Discord server](https://discord.com/invite/b5rXHjAg7A) 82 | - Open a [GitHub support issue](https://github.com/clerkinc/javascript/issues/new?assignees=&labels=question&template=ask_a_question.md&title=Support%3A+) 83 | - Contact options listed on [our Support page](https://clerk.dev/support?utm_source=github&utm_medium=clerk_expo_starter) 84 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "clerk-expo-starter", 4 | "slug": "clerk-expo-starter", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "updates": { 16 | "fallbackToCacheTimeout": 0 17 | }, 18 | "assetBundlePatterns": ["**/*"], 19 | "ios": { 20 | "supportsTablet": true, 21 | "associatedDomains": ["applinks:clerk.helping.lemming-64.lclstage.dev"], 22 | "bundleIdentifier": "dev.clerk.clerkexpostarter" 23 | }, 24 | "android": { 25 | "adaptiveIcon": { 26 | "foregroundImage": "./assets/images/adaptive-icon.png", 27 | "backgroundColor": "#ffffff" 28 | }, 29 | "intentFilters": [ 30 | { 31 | "action": "VIEW", 32 | "autoVerify": true, 33 | "data": [ 34 | { 35 | "scheme": "https", 36 | "host": "clerk.helping.lemming-64.lclstage.dev", 37 | "pathPrefix": "/v1/oauth-native-callback" 38 | } 39 | ], 40 | "category": ["BROWSABLE", "DEFAULT"] 41 | } 42 | ], 43 | "package": "com.sokratisv.clerkexpostarter" 44 | }, 45 | "web": { 46 | "favicon": "./assets/images/favicon.png" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/favicon.png -------------------------------------------------------------------------------- /assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/icon.png -------------------------------------------------------------------------------- /assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/images/favicon.png -------------------------------------------------------------------------------- /assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/images/icon.png -------------------------------------------------------------------------------- /assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/assets/images/splash.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/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 | -------------------------------------------------------------------------------- /cache.ts: -------------------------------------------------------------------------------- 1 | import * as SecureStore from "expo-secure-store"; 2 | import { Platform } from "react-native"; 3 | import { TokenCache } from "@clerk/clerk-expo/dist/cache"; 4 | 5 | const createTokenCache = (): TokenCache => { 6 | return { 7 | getToken: (key) => { 8 | return SecureStore.getItemAsync(key); 9 | }, 10 | saveToken: (key, token) => { 11 | return SecureStore.setItemAsync(key, token); 12 | }, 13 | }; 14 | }; 15 | 16 | // SecureStore is not supported on the web 17 | // https://github.com/expo/expo/issues/7744#issuecomment-611093485 18 | export const tokenCache = Platform.OS !== "web" ? createTokenCache() : undefined; 19 | -------------------------------------------------------------------------------- /components/OAuth.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as WebBrowser from "expo-web-browser"; 3 | import { Text, TouchableOpacity } from "react-native"; 4 | import { useOAuth } from "@clerk/clerk-expo"; 5 | import { styles } from "./Styles"; 6 | import { useWamUpBrowser } from "../hooks/useWarmUpBrowser"; 7 | 8 | WebBrowser.maybeCompleteAuthSession(); 9 | 10 | export function OAuthButtons() { 11 | // Warm up the android browser to improve UX 12 | // https://docs.expo.dev/guides/authentication/#improving-user-experience 13 | useWamUpBrowser(); 14 | 15 | const { startOAuthFlow } = useOAuth({ strategy: "oauth_google" }); 16 | 17 | const onPress = React.useCallback(async () => { 18 | try { 19 | const { createdSessionId, signIn, signUp, setActive } = 20 | await startOAuthFlow(); 21 | 22 | if (createdSessionId) { 23 | setActive({ session: createdSessionId }); 24 | } else { 25 | // Use signIn or signUp for next steps such as MFA 26 | } 27 | } catch (err) { 28 | console.error("OAuth error", err); 29 | } 30 | }, []); 31 | 32 | return ( 33 | 37 | Continue with Google 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /components/Styles.ts: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | 3 | export const styles = StyleSheet.create({ 4 | container: { 5 | flex: 1, 6 | backgroundColor: "#fff", 7 | alignItems: "center", 8 | justifyContent: "flex-start", 9 | paddingTop: 20, 10 | }, 11 | 12 | inputView: { 13 | borderRadius: 5, 14 | width: "90%", 15 | height: 45, 16 | marginBottom: 20, 17 | borderColor: "#000", 18 | borderStyle: "solid", 19 | borderWidth: 1, 20 | }, 21 | 22 | textInput: { 23 | height: 50, 24 | flex: 1, 25 | padding: 10, 26 | marginLeft: 20, 27 | }, 28 | 29 | primaryButton: { 30 | width: "90%", 31 | borderRadius: 5, 32 | height: 50, 33 | alignItems: "center", 34 | justifyContent: "center", 35 | marginTop: 40, 36 | backgroundColor: "#000", 37 | color: "#ffffff", 38 | }, 39 | 40 | primaryButtonText: { 41 | color: "#ffffff", 42 | fontWeight: "bold", 43 | }, 44 | 45 | titleText: { 46 | color: "#000", 47 | fontSize: 20, 48 | fontWeight: "bold", 49 | marginBottom: 20, 50 | }, 51 | 52 | footer: { 53 | color: "#000", 54 | marginTop: 20, 55 | flex: 1, 56 | backgroundColor: "#fff", 57 | alignItems: "center", 58 | justifyContent: "flex-start", 59 | }, 60 | 61 | secondaryButton: { 62 | marginTop: 15, 63 | borderRadius: 5, 64 | padding: 10, 65 | alignItems: "center", 66 | justifyContent: "center", 67 | backgroundColor: "#ffffff", 68 | borderWidth: 1, 69 | borderColor: "#000", 70 | }, 71 | 72 | secondaryButtonText: { 73 | color: "#000", 74 | fontWeight: "bold", 75 | }, 76 | 77 | oauthView: { 78 | width: "90%", 79 | borderBottomWidth: 1, 80 | borderBottomColor: "#000", 81 | marginBottom: 20, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /docs/clerk-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/docs/clerk-logo-dark.png -------------------------------------------------------------------------------- /docs/clerk-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/docs/clerk-logo-light.png -------------------------------------------------------------------------------- /docs/sign-in-oauth-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/docs/sign-in-oauth-demo.gif -------------------------------------------------------------------------------- /docs/sign-up-otp-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/docs/sign-up-otp-demo.gif -------------------------------------------------------------------------------- /docs/whitelist-redirect-urls-for-mobile-oauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-expo-starter/7f7dd9ac44d0f11ee48c51b8fc18580760d742f6/docs/whitelist-redirect-urls-for-mobile-oauth.png -------------------------------------------------------------------------------- /hooks/useCachedResources.ts: -------------------------------------------------------------------------------- 1 | import { FontAwesome } from "@expo/vector-icons"; 2 | import * as Font from "expo-font"; 3 | import * as React from "react"; 4 | 5 | export default function useCachedResources() { 6 | const [isLoadingComplete, setLoadingComplete] = React.useState(false); 7 | 8 | // Load any resources or data that we need prior to rendering the app 9 | React.useEffect(() => { 10 | async function loadResourcesAndDataAsync() { 11 | try { 12 | // Load fonts 13 | await Font.loadAsync({ 14 | ...FontAwesome.font, 15 | "space-mono": require("../assets/fonts/SpaceMono-Regular.ttf"), 16 | }); 17 | } catch (e) { 18 | // We might want to provide this error information to an error reporting service 19 | console.warn(e); 20 | } finally { 21 | setLoadingComplete(true); 22 | } 23 | } 24 | 25 | loadResourcesAndDataAsync(); 26 | }, []); 27 | 28 | return isLoadingComplete; 29 | } 30 | -------------------------------------------------------------------------------- /hooks/useWarmUpBrowser.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as WebBrowser from "expo-web-browser"; 3 | 4 | export const useWamUpBrowser = () => { 5 | React.useEffect(() => { 6 | void WebBrowser.warmUpAsync(); 7 | return () => { 8 | void WebBrowser.coolDownAsync(); 9 | }; 10 | }, []); 11 | }; 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | 3 | import App from "./App"; 4 | 5 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 6 | // It also ensures that whether you load the app in Expo Go or in a native build, 7 | // the environment is set up appropriately 8 | registerRootComponent(App); 9 | -------------------------------------------------------------------------------- /logger.ts: -------------------------------------------------------------------------------- 1 | export function log(msg = "") { 2 | return console.log(new Date() + ":> " + msg); 3 | } 4 | -------------------------------------------------------------------------------- /navigation/LinkingConfiguration.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about deep linking with React Navigation 3 | * https://reactnavigation.org/docs/deep-linking 4 | * https://reactnavigation.org/docs/configuring-links 5 | */ 6 | 7 | import { LinkingOptions } from "@react-navigation/native"; 8 | import * as Linking from "expo-linking"; 9 | 10 | import { RootStackParamList } from "../types"; 11 | 12 | const linking: LinkingOptions = { 13 | prefixes: [Linking.makeUrl("/")], 14 | config: { 15 | screens: { 16 | Root: { 17 | screens: { 18 | SignUp: { 19 | screens: { 20 | SignUpScreen: "SignUp", 21 | }, 22 | }, 23 | SignIn: { 24 | screens: { 25 | SignInScreen: "SignIn", 26 | }, 27 | }, 28 | }, 29 | }, 30 | }, 31 | }, 32 | }; 33 | 34 | export default linking; 35 | -------------------------------------------------------------------------------- /navigation/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * If you are not familiar with React Navigation, refer to the "Fundamentals" guide: 3 | * https://reactnavigation.org/docs/getting-started 4 | * 5 | */ 6 | import { NavigationContainer } from "@react-navigation/native"; 7 | import { createNativeStackNavigator } from "@react-navigation/native-stack"; 8 | import * as React from "react"; 9 | 10 | import SignUpScreen from "../screens/SignUpScreen"; 11 | import SignInScreen from "../screens/SignInScreen"; 12 | import VerifyCodeScreen from "../screens/VerifyCodeScreen"; 13 | import MyProfileScreen from "../screens/MyProfileScreen"; 14 | import { RootStackParamList } from "../types"; 15 | import LinkingConfiguration from "./LinkingConfiguration"; 16 | import { ClerkLoaded, useUser } from "@clerk/clerk-expo"; 17 | import * as SplashScreen from "expo-splash-screen"; 18 | 19 | export default function Navigation() { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | } 26 | 27 | const Stack = createNativeStackNavigator(); 28 | 29 | /** 30 | * Read more about the protected routes pattern in React Native 31 | * 32 | * https://reactnavigation.org/docs/auth-flow 33 | */ 34 | const RootNavigator = () => { 35 | const { isSignedIn, isLoaded } = useUser(); 36 | 37 | React.useEffect(() => { 38 | if (isLoaded) { 39 | SplashScreen.hideAsync(); 40 | } 41 | }, [isLoaded]); 42 | 43 | return ( 44 | 45 | 46 | {isSignedIn ? ( 47 | 52 | ) : ( 53 | <> 54 | 59 | 64 | 69 | 70 | )} 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clerk-expo-starter", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "expo start", 6 | "android": "expo run:android", 7 | "ios": "expo run:ios", 8 | "web": "expo start --web", 9 | "eject": "expo eject", 10 | "test": "jest --watchAll" 11 | }, 12 | "jest": { 13 | "preset": "jest-expo" 14 | }, 15 | "dependencies": { 16 | "@clerk/clerk-expo": "^0.18.12", 17 | "@expo/vector-icons": "13.0.0", 18 | "@react-navigation/bottom-tabs": "6.5.8", 19 | "@react-navigation/native": "6.1.7", 20 | "@react-navigation/native-stack": "6.9.13", 21 | "@types/expo": "33.0.1", 22 | "expo": "~49.0.0", 23 | "expo-asset": "8.10.1", 24 | "expo-auth-session": "5.0.2", 25 | "expo-constants": "14.4.2", 26 | "expo-font": "11.4.0", 27 | "expo-linking": "5.0.2", 28 | "expo-random": "13.2.0", 29 | "expo-secure-store": "12.3.1", 30 | "expo-splash-screen": "0.20.4", 31 | "expo-status-bar": "1.6.0", 32 | "expo-web-browser": "12.3.2", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-native": "0.72.1", 36 | "react-native-safe-area-context": "4.6.3", 37 | "react-native-screens": "3.22.1", 38 | "react-native-url-polyfill": "1.3.0", 39 | "react-native-web": "0.19.6", 40 | "uri-scheme": "1.1.0" 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.22.8", 44 | "@types/react": "^18.2.14", 45 | "@types/react-native": "^0.72.2", 46 | "jest-expo": "^49.0.0", 47 | "prettier": "^2.8.4", 48 | "typescript": "^5.1.6" 49 | }, 50 | "private": true 51 | } 52 | -------------------------------------------------------------------------------- /screens/MyProfileScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; 3 | import { SignedIn, SignedOut, useAuth, useUser } from "@clerk/clerk-expo"; 4 | import { log } from "../logger"; 5 | import { RootStackScreenProps } from "../types"; 6 | 7 | export default function SafeMyProfileScreen( 8 | props: RootStackScreenProps<"MyProfile"> 9 | ) { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | Unauthorized 18 | 19 | 20 | 21 | ); 22 | } 23 | 24 | function MyProfileScreen({ navigation }: RootStackScreenProps<"MyProfile">) { 25 | const { getToken, signOut } = useAuth(); 26 | const { user } = useUser(); 27 | 28 | const [sessionToken, setSessionToken] = React.useState(""); 29 | 30 | const onSignOutPress = async () => { 31 | try { 32 | await signOut(); 33 | } catch (err: any) { 34 | log("Error:> " + err?.status || ""); 35 | log("Error:> " + err?.errors ? JSON.stringify(err.errors) : err); 36 | } 37 | }; 38 | 39 | React.useEffect(() => { 40 | const scheduler = setInterval(async () => { 41 | const token = await getToken(); 42 | setSessionToken(token as string); 43 | }, 1000); 44 | 45 | return () => clearInterval(scheduler); 46 | }, []); 47 | 48 | return ( 49 | 50 | Hello {user?.firstName} 51 | 52 | Sign out 53 | 54 | {sessionToken} 55 | 56 | ); 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | container: { 61 | flex: 1, 62 | alignItems: "center", 63 | justifyContent: "center", 64 | padding: 20, 65 | }, 66 | title: { 67 | fontSize: 20, 68 | fontWeight: "bold", 69 | }, 70 | link: { 71 | marginTop: 15, 72 | paddingVertical: 15, 73 | }, 74 | linkText: { 75 | fontSize: 14, 76 | color: "#2e78b7", 77 | }, 78 | token: { 79 | marginTop: 15, 80 | paddingVertical: 15, 81 | fontSize: 15, 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /screens/SignInScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Text, TextInput, TouchableOpacity, View } from "react-native"; 3 | import { useSignIn } from "@clerk/clerk-expo"; 4 | import { log } from "../logger"; 5 | import { RootStackScreenProps } from "../types"; 6 | import { OAuthButtons } from "../components/OAuth"; 7 | import { styles } from "../components/Styles"; 8 | 9 | export default function SignInScreen({ 10 | navigation, 11 | }: RootStackScreenProps<"SignIn">) { 12 | const { signIn, setSession, isLoaded } = useSignIn(); 13 | 14 | const [emailAddress, setEmailAddress] = React.useState(""); 15 | const [password, setPassword] = React.useState(""); 16 | 17 | const onSignInPress = async () => { 18 | if (!isLoaded) { 19 | return; 20 | } 21 | 22 | try { 23 | const completeSignIn = await signIn.create({ 24 | identifier: emailAddress, 25 | password, 26 | }); 27 | 28 | await setSession(completeSignIn.createdSessionId); 29 | } catch (err: any) { 30 | log("Error:> " + err?.status || ""); 31 | log("Error:> " + err?.errors ? JSON.stringify(err.errors) : err); 32 | } 33 | }; 34 | 35 | const onSignUpPress = () => navigation.replace("SignUp"); 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | setEmailAddress(emailAddress)} 51 | /> 52 | 53 | 54 | 55 | setPassword(password)} 62 | /> 63 | 64 | 65 | 66 | Sign in 67 | 68 | 69 | 70 | Have an account? 71 | 72 | 76 | Sign up 77 | 78 | 79 | 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /screens/SignUpScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, TextInput, TouchableOpacity, View } from "react-native"; 3 | import { useSignUp } from "@clerk/clerk-expo"; 4 | import { log } from "../logger"; 5 | import { RootStackScreenProps } from "../types"; 6 | import { styles } from "../components/Styles"; 7 | import { OAuthButtons } from "../components/OAuth"; 8 | 9 | export default function SignUpScreen({ 10 | navigation, 11 | }: RootStackScreenProps<"SignUp">) { 12 | const { isLoaded, signUp } = useSignUp(); 13 | const [firstName, setFirstName] = React.useState(""); 14 | const [lastName, setLastName] = React.useState(""); 15 | const [emailAddress, setEmailAddress] = React.useState(""); 16 | const [password, setPassword] = React.useState(""); 17 | 18 | const onSignUpPress = async () => { 19 | if (!isLoaded) { 20 | return; 21 | } 22 | 23 | try { 24 | await signUp.create({ 25 | firstName, 26 | lastName, 27 | emailAddress, 28 | password, 29 | }); 30 | 31 | // https://docs.clerk.dev/popular-guides/passwordless-authentication 32 | await signUp.prepareEmailAddressVerification({ strategy: "email_code" }); 33 | 34 | navigation.navigate("VerifyCode"); 35 | } catch (err: any) { 36 | log("Error:> " + err?.status || ""); 37 | log("Error:> " + err?.errors ? JSON.stringify(err.errors) : err); 38 | } 39 | }; 40 | 41 | const onSignInPress = () => navigation.replace("SignIn"); 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | setFirstName(firstName)} 56 | /> 57 | 58 | 59 | 60 | setLastName(lastName)} 66 | /> 67 | 68 | 69 | 70 | setEmailAddress(email)} 77 | /> 78 | 79 | 80 | 81 | setPassword(password)} 88 | /> 89 | 90 | 91 | 92 | Sign up 93 | 94 | 95 | 96 | Have an account? 97 | 98 | 102 | Sign in 103 | 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /screens/VerifyCodeScreen.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Text, TextInput, TouchableOpacity, View } from "react-native"; 3 | import { useClerk, useSignUp } from "@clerk/clerk-expo"; 4 | import { RootStackScreenProps } from "../types"; 5 | import { styles } from "../components/Styles"; 6 | import { log } from "../logger"; 7 | 8 | export default function SignUpScreen({ 9 | navigation, 10 | }: RootStackScreenProps<"VerifyCode">) { 11 | const { isLoaded, signUp, setSession } = useSignUp(); 12 | 13 | const [code, setCode] = React.useState(""); 14 | 15 | const onPress = async () => { 16 | if (!isLoaded) { 17 | return; 18 | } 19 | 20 | try { 21 | const completeSignUp = await signUp.attemptEmailAddressVerification({ 22 | code, 23 | }); 24 | 25 | await setSession(completeSignUp.createdSessionId); 26 | } catch (err: any) { 27 | log("Error:> " + err?.status || ""); 28 | log("Error:> " + err?.errors ? JSON.stringify(err.errors) : err); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | 35 | setCode(code)} 41 | /> 42 | 43 | 44 | Verify Email 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /types.tsx: -------------------------------------------------------------------------------- 1 | import { NativeStackScreenProps } from "@react-navigation/native-stack"; 2 | 3 | declare global { 4 | namespace ReactNavigation { 5 | interface RootParamList extends RootStackParamList {} 6 | } 7 | } 8 | 9 | export type RootStackParamList = { 10 | Root: undefined; 11 | SignUp: undefined; 12 | SignIn: undefined; 13 | MyProfile: undefined; 14 | VerifyCode: undefined; 15 | }; 16 | 17 | export type RootStackScreenProps = 18 | NativeStackScreenProps; 19 | --------------------------------------------------------------------------------