Skip to content

Commit

Permalink
Add clerk auth (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
KaBankz authored Feb 6, 2025
1 parent 3df340f commit 679dee4
Show file tree
Hide file tree
Showing 21 changed files with 4,593 additions and 20 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Batteries included, hyper opinionated, Expo React Native project template.
- [x] GitHub Workflows
- [x] Cross Platform (iOS, Android, Web)
- [x] i18n w/ lingui
- [x] Auth w/ Clerk

## Future Features

Expand All @@ -27,3 +28,10 @@ Batteries included, hyper opinionated, Expo React Native project template.
- [ ] EAS Config

and more...

## Setup Auth

1. Create a new Clerk project
2. Add your clerk pushable key to the `.env` file
3. Disable clerk's bot protection (does not work on native)
4. Enable SSO for Google and Apple
1 change: 1 addition & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'expo-font',
'expo-router',
'expo-dev-client',
'expo-secure-store',
'expo-localization',
[
'expo-splash-screen',
Expand Down
3,632 changes: 3,632 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[tools]
node = "22.13.1"
bun = "1.2.2"

[env]
EXPO_DEBUG = 0
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"nuke": "git clean -xdf .expo ios android dist node_modules"
},
"dependencies": {
"@clerk/clerk-expo": "^2.7.5",
"@expo/vector-icons": "^14.0.4",
"@lingui/react": "^5.2.0",
"@react-navigation/native": "^7.0.14",
Expand All @@ -35,6 +36,7 @@
"expo-linking": "~7.0.5",
"expo-localization": "~16.0.1",
"expo-router": "~4.0.17",
"expo-secure-store": "^14.0.1",
"expo-splash-screen": "~0.29.21",
"expo-status-bar": "~2.0.1",
"expo-symbols": "~0.2.2",
Expand Down
11 changes: 11 additions & 0 deletions src/app/(auth)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Redirect, Stack } from 'expo-router';

import { useAuth } from '@clerk/clerk-expo';

export default function AuthLayout() {
const { isSignedIn } = useAuth();

if (isSignedIn) return <Redirect href='/' />;

return <Stack screenOptions={{ headerShown: false }} />;
}
1 change: 1 addition & 0 deletions src/app/(auth)/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/screens/sign-in';
1 change: 1 addition & 0 deletions src/app/(auth)/sign-up.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@/screens/sign-up';
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { Image, Platform, StyleSheet } from 'react-native';

import { useClerk, useUser } from '@clerk/clerk-expo';
import { Trans } from '@lingui/react/macro';

import PartialReactLogo from '@/assets/images/partial-react-logo.png';
import { HelloWave } from '@/components/HelloWave';
import ParallaxScrollView from '@/components/ParallaxScrollView';
import { Pressable } from '@/components/Pressable';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';

export default function HomeScreen() {
const { user } = useUser();
const { signOut } = useClerk();

console.log(user);

return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
Expand All @@ -17,10 +24,17 @@ export default function HomeScreen() {
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type='title'>
<Trans>Welcome!</Trans>
<Trans>Welcome! {user?.fullName}</Trans>

Check warning on line 27 in src/app/(protected)/(tabs)/index.tsx

View workflow job for this annotation

GitHub Actions / lint

Should be ${variable}, not ${object.property} or ${myFunction()}
</ThemedText>
<HelloWave />
</ThemedView>

<Pressable onPress={() => void signOut()}>
<ThemedText type='subtitle'>
<Trans>Sign out</Trans>
</ThemedText>
</Pressable>

<ThemedView style={styles.stepContainer}>
<ThemedText type='subtitle'>
<Trans>Step 1: Try it</Trans>
Expand Down
18 changes: 18 additions & 0 deletions src/app/(protected)/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Redirect } from 'expo-router';
import { Stack } from 'expo-router/stack';

import { useAuth } from '@clerk/clerk-expo';

export default function HomeLayout() {
const { isSignedIn } = useAuth();

if (!isSignedIn) {
return <Redirect href='/sign-in' />;
}

return (
<Stack>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
</Stack>
);
}
26 changes: 16 additions & 10 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useEffect } from 'react';
import { useFonts, type FontSource } from 'expo-font';
import { Stack } from 'expo-router';
import { Slot } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';

import { ClerkLoaded, ClerkProvider } from '@clerk/clerk-expo';
import {
DarkTheme,
DefaultTheme,
Expand All @@ -12,6 +13,7 @@ import {

import { useColorScheme } from '@/hooks/useColorScheme';
import { LinguiProvider } from '@/i18n';
import { tokenCache } from '@/lib/clerkCache';

import '@/app/global.css';
import '@/i18n';
Expand All @@ -36,14 +38,18 @@ export default function RootLayout() {
}

return (
<LinguiProvider>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name='(tabs)' options={{ headerShown: false }} />
<Stack.Screen name='+not-found' />
</Stack>
<StatusBar style='auto' />
</ThemeProvider>
</LinguiProvider>
<ClerkProvider
tokenCache={tokenCache}
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!}>
<ClerkLoaded>
<LinguiProvider>
<ThemeProvider
value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<StatusBar style='auto' />
<Slot />
</ThemeProvider>
</LinguiProvider>
</ClerkLoaded>
</ClerkProvider>
);
}
28 changes: 28 additions & 0 deletions src/lib/clerkCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as SecureStore from 'expo-secure-store';

const createTokenCache = () => {
return {
getToken: async (key: string) => {
try {
const item = await SecureStore.getItemAsync(key);
if (item) {
console.log(`${key} was used 🔐 \n`);
} else {
console.log('No values stored under key: ' + key);
}
return item;
} catch (error) {
console.error('secure store get item error: ', error);
await SecureStore.deleteItemAsync(key);
return null;
}
},
saveToken: (key: string, token: string) => {
return SecureStore.setItemAsync(key, token);
},
};
};

// SecureStore is not supported on the web
export const tokenCache =
process.env.EXPO_OS !== 'web' ? createTokenCache() : undefined;
189 changes: 189 additions & 0 deletions src/screens/sign-in.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useEffect, useState } from 'react';
import { Text, TextInput, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Link, useRouter } from 'expo-router';
import * as WebBrowser from 'expo-web-browser';

import { useSignIn, useSSO } from '@clerk/clerk-expo';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';

import { Pressable } from '@/components/Pressable';

export const useWarmUpBrowser = () => {
useEffect(() => {
// Preloads the browser for Android devices to reduce authentication load time
// See: https://docs.expo.dev/guides/authentication/#improving-user-experience
void WebBrowser.warmUpAsync();
return () => {
// Cleanup: closes browser when component unmounts
void WebBrowser.coolDownAsync();
};
}, []);
};

// Handle any pending authentication sessions
WebBrowser.maybeCompleteAuthSession();

export default function SignInScreen() {
useWarmUpBrowser();

const { startSSOFlow } = useSSO();
const { signIn, setActive, isLoaded } = useSignIn();
const router = useRouter();

const [emailAddress, setEmailAddress] = useState('');
const [password, setPassword] = useState('');

// Handle the submission of the sign-in form
const onSignInPress = async () => {
if (!isLoaded) return;

// Start the sign-in process using the email and password provided
try {
const signInAttempt = await signIn.create({
identifier: emailAddress,
password,
});

// If sign-in process is complete, set the created session as active
// and redirect the user
if (signInAttempt.status === 'complete') {
await setActive({ session: signInAttempt.createdSessionId });
router.replace('/');
} else {
// If the status isn't complete, check why. User might need to
// complete further steps.
console.error(JSON.stringify(signInAttempt, null, 2));
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2));
}
};

const onPressGoogle = async () => {
try {
// Start the authentication process by calling `startSSOFlow()`
const { createdSessionId, setActive } = await startSSOFlow({
strategy: 'oauth_google',
});

// If sign in was successful, set the active session
if (createdSessionId) {
await setActive!({ session: createdSessionId });
router.replace('/');
} else {
// If there is no `createdSessionId`,
// there are missing requirements, such as MFA
// Use the `signIn` or `signUp` returned from `startSSOFlow`
// to handle next steps
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2));
}
};

const onPressApple = async () => {
try {
// Start the authentication process by calling `startSSOFlow()`
const { createdSessionId, setActive } = await startSSOFlow({
strategy: 'oauth_apple',
});

// If sign in was successful, set the active session
if (createdSessionId) {
await setActive!({ session: createdSessionId });
router.replace('/');
} else {
// If there is no `createdSessionId`,
// there are missing requirements, such as MFA
// Use the `signIn` or `signUp` returned from `startSSOFlow`
// to handle next steps
}
} catch (err) {
// See https://clerk.com/docs/custom-flows/error-handling
// for more info on error handling
console.error(JSON.stringify(err, null, 2));
}
};

return (
<SafeAreaView className='flex-1'>
<View className='flex-1 justify-center px-6'>
<View className='rounded-2xl p-8'>
<Text className='mb-8 text-center text-2xl font-bold text-white'>
<Trans>Welcome Back</Trans>
</Text>

<View className='space-y-4'>
<View>
<TextInput
className='w-full rounded-lg border px-4 py-3 text-white'
autoCapitalize='none'
value={emailAddress}
placeholder={t`Enter email`}
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
placeholderTextColor='#9CA3AF'
/>
</View>

<View>
<TextInput
className='w-full rounded-lg border px-4 py-3 text-white'
value={password}
placeholder={t`Enter password`}
secureTextEntry={true}
onChangeText={(password) => setPassword(password)}
placeholderTextColor='#9CA3AF'
/>
</View>

<Pressable
className='w-full rounded-lg py-3'
style={{ backgroundColor: '#2563EB' }}
onPress={() => void onSignInPress()}>
<Text className='text-center text-base font-semibold text-white'>
<Trans>Sign in</Trans>
</Text>
</Pressable>

<Pressable
className='w-full rounded-lg py-3'
style={{ backgroundColor: '#2563EB' }}
onPress={() => void onPressGoogle()}>
<Text className='text-center text-base font-semibold text-white'>
<Trans>Sign in with Google</Trans>
</Text>
</Pressable>

<Pressable
className='w-full rounded-lg py-3'
style={{ backgroundColor: '#2563EB' }}
onPress={() => void onPressApple()}>
<Text className='text-center text-base font-semibold text-white'>
<Trans>Sign in with Apple</Trans>
</Text>
</Pressable>
</View>
</View>

<View className='mt-8 flex-row items-center justify-center space-x-1'>
<Text className='text-white'>
<Trans>Don't have an account? </Trans>
</Text>
<Link href='/sign-up' asChild replace>
<Pressable>
<Text className='font-semibold text-blue-600'>
<Trans>Sign up</Trans>
</Text>
</Pressable>
</Link>
</View>
</View>
</SafeAreaView>
);
}
Loading

0 comments on commit 679dee4

Please sign in to comment.