Skip to content

Commit

Permalink
feat(mobile): setup clerk email auth and integrate with api rpc (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
bkdev98 authored Jun 9, 2024
1 parent 63560e8 commit c330acc
Show file tree
Hide file tree
Showing 19 changed files with 1,047 additions and 215 deletions.
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "@6pm/api",
"version": "0.0.0",
"license": "GPL-3.0",
"main": "index.ts",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
5 changes: 3 additions & 2 deletions apps/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"composite": true
},
"include": [
"next-env.d.ts",
Expand All @@ -27,4 +28,4 @@
"exclude": [
"node_modules"
]
}
}
3 changes: 3 additions & 0 deletions apps/mobile/.env.example
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=
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ import { Tabs } from 'expo-router';
import React from 'react';

import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';

export default function TabLayout() {
const colorScheme = useColorScheme();

return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
// tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
}}>
<Tabs.Screen
Expand Down
47 changes: 47 additions & 0 deletions apps/mobile/app/(app)/(tabs)/explore.tsx
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>
);
}
7 changes: 7 additions & 0 deletions apps/mobile/app/(app)/(tabs)/index.tsx
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>
);
}
17 changes: 17 additions & 0 deletions apps/mobile/app/(app)/_layout.tsx
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 />;
};
19 changes: 19 additions & 0 deletions apps/mobile/app/(auth)/_layout.tsx
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>
);
};
12 changes: 12 additions & 0 deletions apps/mobile/app/(auth)/login.tsx
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>
)
}
25 changes: 0 additions & 25 deletions apps/mobile/app/(tabs)/explore.tsx

This file was deleted.

7 changes: 0 additions & 7 deletions apps/mobile/app/(tabs)/index.tsx

This file was deleted.

42 changes: 28 additions & 14 deletions apps/mobile/app/_layout.tsx
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>
);
}
114 changes: 114 additions & 0 deletions apps/mobile/components/auth/auth-email.tsx
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} />
</>
)
}
25 changes: 25 additions & 0 deletions apps/mobile/lib/cache.ts
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;
Loading

0 comments on commit c330acc

Please sign in to comment.