-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(mobile): setup clerk email auth and integrate with api rpc
- Loading branch information
Quốc Khánh
authored and
Quốc Khánh
committed
Jun 9, 2024
1 parent
fabd774
commit 0ee1432
Showing
19 changed files
with
1,047 additions
and
215 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
EXPO_USE_METRO_WORKSPACE_ROOT=1 | ||
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= | ||
EXPO_PUBLIC_API_URL= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/Avatar'; | ||
import { Button } from '@/components/Button'; | ||
import { useAuth } from '@clerk/clerk-expo'; | ||
import { ScrollView, Text, View } from 'react-native'; | ||
import { getHonoClient } from '@/lib/client'; | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
export default function TabTwoScreen() { | ||
const { signOut } = useAuth(); | ||
const { data } = useQuery({ | ||
queryKey: ['me'], | ||
queryFn: async () => { | ||
const hc = await getHonoClient() | ||
const res = await hc.v1.auth.me.$get() | ||
if (res.ok) { | ||
return await res.json() | ||
} else { | ||
throw new Error(await res.text()) | ||
} | ||
}, | ||
}) | ||
|
||
return ( | ||
<ScrollView contentContainerClassName='flex-1 p-4'> | ||
<View className="flex justify-center flex-1 items-center flex-row gap-4"> | ||
<Avatar className="h-14 w-14"> | ||
<AvatarImage | ||
source={{ | ||
uri: 'https://avatars.githubusercontent.com/u/16166195?s=96&v=4', | ||
}} | ||
/> | ||
<AvatarFallback>CG</AvatarFallback> | ||
</Avatar> | ||
<Avatar className="h-14 w-14"> | ||
<AvatarImage | ||
source={{ | ||
uri: 'https://avatars.githubusercontent.com/u/9253690?s=96&v=4', | ||
}} | ||
/> | ||
<AvatarFallback>SS</AvatarFallback> | ||
</Avatar> | ||
</View> | ||
<Text>{data?.email ? `Logged as ${data.email}` : 'loading...'}</Text> | ||
<Button label="Sign Out" onPress={() => signOut()} /> | ||
</ScrollView> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Text } from 'react-native'; | ||
|
||
export default function HomeScreen() { | ||
return ( | ||
<Text>Home Screen</Text> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Redirect, Stack } from 'expo-router'; | ||
import { useAuth } from '@clerk/clerk-expo'; | ||
import { Text } from 'react-native'; | ||
|
||
export default function AuthenticatedLayout() { | ||
const { isLoaded, isSignedIn } = useAuth(); | ||
|
||
if (!isLoaded) { | ||
return <Text>Loading...</Text>; | ||
} | ||
|
||
if (!isSignedIn) { | ||
return <Redirect href={"/login"} />; | ||
} | ||
|
||
return <Stack />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Redirect, Stack } from 'expo-router'; | ||
import { useAuth } from '@clerk/clerk-expo'; | ||
import { SafeAreaView } from 'react-native'; | ||
|
||
export default function UnAuthenticatedLayout() { | ||
const { isSignedIn, userId } = useAuth(); | ||
|
||
console.log("UnAuthenticatedLayout", isSignedIn, userId) | ||
|
||
if (isSignedIn) { | ||
return <Redirect href={"/"} />; | ||
} | ||
|
||
return ( | ||
<SafeAreaView className="flex-1"> | ||
<Stack screenOptions={{ headerShown: false }} /> | ||
</SafeAreaView> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { AuthEmail } from "@/components/auth/auth-email"; | ||
import { ScrollView, Text } from "react-native"; | ||
|
||
export default function LoginScreen() { | ||
return ( | ||
<ScrollView className="bg-card p-4" contentContainerClassName="gap-4"> | ||
<Text className="text-3xl font-semibold">Manage your expense seamlessly</Text> | ||
<Text className="text-muted-foreground">Let 6pm a good time to spend</Text> | ||
<AuthEmail /> | ||
</ScrollView> | ||
) | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,53 @@ | ||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; | ||
import { useFonts } from 'expo-font'; | ||
import { Stack } from 'expo-router'; | ||
import { Slot } from 'expo-router'; | ||
import * as SplashScreen from 'expo-splash-screen'; | ||
import { useEffect } from 'react'; | ||
import 'react-native-reanimated'; | ||
import { ClerkProvider } from '@clerk/clerk-expo'; | ||
import { tokenCache } from '@/lib/cache'; | ||
|
||
import 'react-native-reanimated'; | ||
import { useColorScheme } from '@/hooks/useColorScheme'; | ||
|
||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; | ||
import "../global.css" | ||
import { SafeAreaProvider } from 'react-native-safe-area-context'; | ||
import { QueryClientProvider } from '@tanstack/react-query'; | ||
import { queryClient } from '@/lib/client'; | ||
|
||
// Prevent the splash screen from auto-hiding before asset loading is complete. | ||
SplashScreen.preventAutoHideAsync(); | ||
|
||
export const unstable_settings = { | ||
initialRouteName: '(app)', | ||
}; | ||
|
||
export default function RootLayout() { | ||
const colorScheme = useColorScheme(); | ||
const [loaded] = useFonts({ | ||
const [fontLoaded] = useFonts({ | ||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), | ||
}); | ||
|
||
useEffect(() => { | ||
if (loaded) { | ||
if (fontLoaded) { | ||
SplashScreen.hideAsync(); | ||
} | ||
}, [loaded]); | ||
}, [fontLoaded]); | ||
|
||
if (!loaded) { | ||
if (!fontLoaded) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||
<Stack> | ||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||
<Stack.Screen name="+not-found" /> | ||
</Stack> | ||
</ThemeProvider> | ||
<QueryClientProvider client={queryClient}> | ||
<ClerkProvider | ||
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY!} | ||
tokenCache={tokenCache} | ||
> | ||
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||
<SafeAreaProvider> | ||
<Slot /> | ||
</SafeAreaProvider> | ||
</ThemeProvider> | ||
</ClerkProvider> | ||
</QueryClientProvider> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { useSignIn, useSignUp } from "@clerk/clerk-expo" | ||
import { Input } from "../Input" | ||
import { Button } from "../Button" | ||
import { useState } from "react"; | ||
import type { EmailCodeFactor } from '@clerk/types'; | ||
import { getHonoClient } from "@/lib/client"; | ||
|
||
export function AuthEmail() { | ||
const [emailAddress, setEmailAddress] = useState(""); | ||
const [code, setCode] = useState(""); | ||
const [verifying, setVerifying] = useState(false); | ||
const [mode, setMode] = useState<"signUp" | "signIn">("signUp"); | ||
|
||
const { isLoaded: isSignUpLoaded, signUp, setActive: setActiveSignUp } = useSignUp() | ||
const { isLoaded: isSignInLoaded, signIn, setActive: setActiveSignIn } = useSignIn() | ||
|
||
if (!isSignUpLoaded || !isSignInLoaded) { | ||
return null | ||
} | ||
|
||
const onContinue = async () => { | ||
try { | ||
await signUp.create({ | ||
emailAddress, | ||
}) | ||
await signUp.prepareEmailAddressVerification() | ||
setVerifying(true); | ||
} catch (err: any) { | ||
if (err?.errors?.[0]?.code === 'form_identifier_exists') { | ||
// If the email address already exists, try to sign in instead | ||
setMode('signIn') | ||
try { | ||
const { supportedFirstFactors } = await signIn.create({ | ||
identifier: emailAddress, | ||
}) | ||
|
||
const emailCodeFactor = supportedFirstFactors.find(i => i.strategy === 'email_code') | ||
if (emailCodeFactor) { | ||
await signIn.prepareFirstFactor({ | ||
strategy: 'email_code', | ||
emailAddressId: (emailCodeFactor as EmailCodeFactor).emailAddressId, | ||
}) | ||
setVerifying(true); | ||
} | ||
} catch (err: any) { | ||
console.log('error', JSON.stringify(err, null, 2)) | ||
} | ||
} else { | ||
console.log('error', JSON.stringify(err, null, 2)) | ||
} | ||
} | ||
} | ||
|
||
const onVerify = async () => { | ||
try { | ||
if (mode === 'signUp') { | ||
const signUpAttempt = await signUp.attemptEmailAddressVerification({ code }) | ||
if (signUpAttempt.status === 'complete') { | ||
await setActiveSignUp({ session: signUpAttempt.createdSessionId }); | ||
console.log('signed up') | ||
// create user | ||
const hc = await getHonoClient() | ||
await hc.v1.users.$post({ | ||
json: { | ||
email: emailAddress, | ||
name: "***" | ||
} | ||
}) | ||
} else { | ||
console.error(signUpAttempt); | ||
} | ||
} else { | ||
const signInAttempt = await signIn.attemptFirstFactor({ strategy: 'email_code', code }) | ||
if (signInAttempt.status === 'complete') { | ||
await setActiveSignIn({ session: signInAttempt.createdSessionId }); | ||
console.log('signed in') | ||
} else { | ||
console.error(signInAttempt); | ||
} | ||
} | ||
} catch (err: any) { | ||
console.log('error', JSON.stringify(err, null, 2)) | ||
} | ||
} | ||
|
||
if (verifying) { | ||
return ( | ||
<> | ||
<Input | ||
placeholder="Enter the code" | ||
keyboardType="number-pad" | ||
value={code} | ||
onChangeText={setCode} | ||
autoFocus | ||
/> | ||
<Button label="Verify" onPress={onVerify} /> | ||
</> | ||
) | ||
} | ||
|
||
return ( | ||
<> | ||
<Input | ||
placeholder="Enter your email address" | ||
keyboardType="email-address" | ||
autoCapitalize="none" | ||
autoFocus | ||
value={emailAddress} | ||
onChangeText={setEmailAddress} | ||
/> | ||
<Button label="Continue" onPress={onContinue} /> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as SecureStore from "expo-secure-store"; | ||
import { Platform } from "react-native"; | ||
import { TokenCache } from "@clerk/clerk-expo/dist/cache"; | ||
|
||
const createTokenCache = (): TokenCache => { | ||
return { | ||
getToken: async (key: string) => { | ||
try { | ||
return await SecureStore.getItemAsync(key); | ||
} 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 | ||
// https://github.com/expo/expo/issues/7744#issuecomment-611093485 | ||
export const tokenCache = | ||
Platform.OS !== "web" ? createTokenCache() : undefined; |
Oops, something went wrong.