All 3 auth methods work & update public profile table as well

This commit is contained in:
Gabriel Brown 2025-03-10 13:59:04 -05:00
parent 07bf94d393
commit f9fd5dafc5
6 changed files with 140 additions and 83 deletions

View File

@ -1,7 +1,7 @@
{ {
"expo": { "expo": {
"name": "Tech Tracker", "name": "Tech Tracker",
"slug": "tech-tracker", "slug": "tech-tracker-expo",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import * as AppleAuthentication from 'expo-apple-authentication' import * as AppleAuthentication from 'expo-apple-authentication';
import { useColorScheme } from '@/hooks/useColorScheme'; import { useColorScheme } from '@/hooks/useColorScheme';
import { ThemedView } from '@/components/theme'; import { ThemedView } from '@/components/theme';
import { StyleSheet, Platform } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications'; import * as Notifications from 'expo-notifications';
import type { updateUser } from '@/constants/Types';
const AppleSignInButton = () => { const AppleSignInButton = () => {
const scheme = useColorScheme() ?? 'dark'; const scheme = useColorScheme() ?? 'dark';
@ -21,18 +22,14 @@ const AppleSignInButton = () => {
//const projectId = Constants.expoConfig?.extra?.projectId; //const projectId = Constants.expoConfig?.extra?.projectId;
//if (!projectId) throw new Error('No projectId found in expo.config.json'); //if (!projectId) throw new Error('No projectId found in expo.config.json');
//const pushToken = await Notifications.getExpoPushTokenAsync({ //const pushToken = await Notifications.getExpoPushTokenAsync({
//projectId, //projectId,
//}); //});
if (credential.identityToken) { if (credential.identityToken) {
const email = credential.email; const email = credential.email;
const full_name = ( const full_name =
credential.fullName && credential.fullName && credential.fullName.givenName && credential.fullName.familyName
credential.fullName.givenName && ? `${credential.fullName.givenName} ${credential.fullName.familyName}`
credential.fullName.familyName : null;
)
? `${credential.fullName.givenName} ${credential.fullName.familyName}`
: null;
const { const {
error, error,
data: { user, session }, data: { user, session },
@ -40,32 +37,32 @@ const AppleSignInButton = () => {
provider: 'apple', provider: 'apple',
token: credential.identityToken, token: credential.identityToken,
}); });
console.log(JSON.stringify({ error, user }, null, 2)) console.log(JSON.stringify({ error, user }, null, 2));
if (!error) { if (!error && session) {
if (user && session) { if (email) {
const data: any = {}; const data: updateUser = {
if (email) data.email = email; email,
if (full_name) data.full_name = full_name; full_name: full_name ?? '',
const { error: updateError } = await supabase.auth.updateUser({ };
const { error: authUpdateError } = await supabase.auth.updateUser({
data, data,
})
const { error: updateError } = await supabase
.from('profiles')
.upsert({
id: session.user.id,
full_name,
email,
provider: 'apple',
updated_at: new Date(),
}); });
if (updateError) { if (updateError) {
console.error('Error updating user metadata:', updateError); console.error('Error updating user metadata:', updateError);
} }
const { error: updateProfileError } =
await supabase.from('profiles').upsert({
id: session.user.id,
username: data?.email.split('@')[0],
full_name: data?.full_name,
updated_at: new Date(),
});
if (updateProfileError) {
console.error('Error updating profile:', updateProfileError);
}
} }
} }
} else { } else {
throw new Error('No identityToken.') throw new Error('No identityToken.');
} }
} catch (e: any) { } catch (e: any) {
if (e.code === 'ERR_REQUEST_CANCELED') { if (e.code === 'ERR_REQUEST_CANCELED') {
@ -76,26 +73,26 @@ const AppleSignInButton = () => {
console.log('Error signing in with Apple:', e); console.log('Error signing in with Apple:', e);
} }
} }
} };
if (Platform.OS !== 'ios') return <ThemedView />; if (Platform.OS !== 'ios') return <ThemedView />;
else return ( else
return (
<ThemedView style={styles.verticallySpaced}> <ThemedView style={styles.verticallySpaced}>
<AppleAuthentication.AppleAuthenticationButton <AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN} buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
buttonStyle={ buttonStyle={
scheme === 'light' scheme === 'light'
? AppleAuthentication.AppleAuthenticationButtonStyle.BLACK ? AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
: AppleAuthentication.AppleAuthenticationButtonStyle.WHITE : AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
} }
cornerRadius={5} cornerRadius={5}
style={{ width: 200, height: 64 }} style={{ width: 200, height: 64 }}
onPress={signInWithApple} onPress={signInWithApple}
/> />
</ThemedView> </ThemedView>
); );
} };
export default AppleSignInButton; export default AppleSignInButton;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -105,4 +102,4 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
marginTop: 20, marginTop: 20,
}, },
}); });

View File

@ -4,6 +4,7 @@ import { supabase } from '@/lib/supabase';
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme'; import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
import AppleSignInButton from '@/components/auth/AppleSignIniOS'; import AppleSignInButton from '@/components/auth/AppleSignIniOS';
import AzureSignIn from './AzureSignIn'; import AzureSignIn from './AzureSignIn';
import type { updateUser } from '@/constants/Types';
// Tells Supabase Auth to continuously refresh the session automatically if // Tells Supabase Auth to continuously refresh the session automatically if
// the app is in the foreground. When this is added, you will continue to receive // the app is in the foreground. When this is added, you will continue to receive
@ -20,6 +21,7 @@ if (Platform.OS !== 'web') {
} }
const Auth = () => { const Auth = () => {
const [full_name, setFullName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -50,11 +52,26 @@ const Auth = () => {
data: { session }, data: { session },
error, error,
} = await supabase.auth.signUp({ } = await supabase.auth.signUp({
email: email, email,
password: password, password,
}); });
if (error) Alert.alert(error.message); if (error) Alert.alert(error.message);
else if (!session) Alert.alert('Please check your inbox for email verification!'); else if (!session) Alert.alert('Please check your inbox for email verification!');
else {
const { error: updateProfileError } = await supabase
.from('profiles')
.upsert({
id: session.user.id,
full_name,
email,
provider: 'email',
updated_at: new Date(),
});
if (updateProfileError) {
Alert.alert('Error updating profile:', updateProfileError.message);
console.error('Error updating profile:', updateProfileError.message);
}
}
setLoading(false); setLoading(false);
}; };
@ -67,6 +84,15 @@ const Auth = () => {
</ThemedText> </ThemedText>
</ThemedView> </ThemedView>
<ThemedView style={styles.verticallySpaced}>
<ThemedTextInput
fontSize={24}
onChangeText={(text) => setFullName(text)}
value={full_name}
placeholder='Full Name'
/>
</ThemedView>
<ThemedView style={[styles.verticallySpaced]}> <ThemedView style={[styles.verticallySpaced]}>
<ThemedTextInput <ThemedTextInput
fontSize={24} fontSize={24}

View File

@ -7,6 +7,7 @@ import * as QueryParams from 'expo-auth-session/build/QueryParams';
import { supabase } from '@/lib/supabase'; import { supabase } from '@/lib/supabase';
import { ThemedView, ThemedButton, ThemedText } from '@/components/theme'; import { ThemedView, ThemedButton, ThemedText } from '@/components/theme';
import { Colors } from '@/constants/Colors'; import { Colors } from '@/constants/Colors';
import type { updateUser } from '@/constants/Types';
WebBrowser.maybeCompleteAuthSession(); WebBrowser.maybeCompleteAuthSession();
@ -27,8 +28,6 @@ const AzureSignIn = () => {
const signInWithAzure = async () => { const signInWithAzure = async () => {
try { try {
setLoading(true); setLoading(true);
console.log('Starting Azure sign-in with tenant-specific endpoint');
console.log('Redirect URI:', redirectUri);
// Create the MSAL auth request // Create the MSAL auth request
const request = new AuthSession.AuthRequest({ const request = new AuthSession.AuthRequest({
@ -44,31 +43,27 @@ const AzureSignIn = () => {
console.log('Generated auth URL:', authUrl); console.log('Generated auth URL:', authUrl);
// Open the auth URL in a browser // Open the auth URL in a browser
const result = await WebBrowser.openAuthSessionAsync( const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri, {
authUrl, showInRecents: true,
redirectUri, });
{
showInRecents: true,
}
);
console.log('Auth session result type:', result.type); console.log('Auth session result type:', result.type);
if (result.type === 'success' && result.url) { if (result.type === 'success' && result.url) {
// Parse the URL to get the authorization code // Parse the URL to get the authorization code
const { params, errorCode } = QueryParams.getQueryParams(result.url); const { params, errorCode } = QueryParams.getQueryParams(result.url);
if (errorCode || params.error) { if (errorCode || params.error) {
const errorMessage = params.error_description || params.error || errorCode; const errorMessage = params.error_description || params.error || errorCode;
throw new Error(`Error during authentication: ${errorMessage}`); throw new Error(`Error during authentication: ${errorMessage}`);
} }
if (!params.code) { if (!params.code) {
throw new Error('No authorization code received'); throw new Error('No authorization code received');
} }
console.log('Authorization code received'); console.log('Authorization code received');
// Exchange the code for tokens // Exchange the code for tokens
const tokenResult = await AuthSession.exchangeCodeAsync( const tokenResult = await AuthSession.exchangeCodeAsync(
{ {
@ -79,26 +74,54 @@ const AzureSignIn = () => {
code_verifier: request.codeVerifier || '', code_verifier: request.codeVerifier || '',
}, },
}, },
discovery discovery,
); );
console.log('Token exchange successful'); console.log('Token exchange successful');
if (!tokenResult.idToken) { if (!tokenResult.idToken) {
throw new Error('No ID token received'); throw new Error('No ID token received');
} }
// Now use the ID token to sign in with Supabase // Now use the ID token to sign in with Supabase
const { data, error } = await supabase.auth.signInWithIdToken({ const { data, error } = await supabase.auth.signInWithIdToken({
provider: 'azure', provider: 'azure',
token: tokenResult.idToken, token: tokenResult.idToken,
}); });
// Check if profies table already has info (User is signing in, not signing up)
const { data: profileData, error: profileError } = await supabase
.from('profiles')
.select('*')
.eq('id', data.user?.id)
.single();
if (profileData.email === '' || !profileData.email && data.session?.user.email) {
const updateData: updateUser = {
email: data.session?.user.email ?? '',
};
const { error: updateAuthError } = await supabase.auth.updateUser({
data: updateData,
});
if (updateAuthError)
Alert.alert('Error updating auth info:', updateAuthError.message);
const { error: updateProfileError } = await supabase
.from('profiles')
.upsert({
id: data.session?.user.id ?? '',
email: data.session?.user.email ?? '',
provider: 'azure',
updated_at: new Date(),
});
if (updateProfileError)
Alert.alert('Error updating profile:', updateProfileError.message);
}
if (error) { if (error) {
console.error('Supabase sign-in error:', error); console.error('Supabase sign-in error:', error);
throw error; throw error;
} }
console.log('Successfully signed in with Azure via Supabase'); console.log('Successfully signed in with Azure via Supabase');
return data; return data;
} else { } else {
@ -114,16 +137,13 @@ const AzureSignIn = () => {
return ( return (
<ThemedView style={styles.verticallySpaced}> <ThemedView style={styles.verticallySpaced}>
<ThemedButton <ThemedButton disabled={loading} onPress={signInWithAzure}>
disabled={loading}
onPress={signInWithAzure}
>
<ThemedText <ThemedText
lightColor={Colors.dark.text} lightColor={Colors.dark.text}
darkColor={Colors.light.text} darkColor={Colors.light.text}
type="defaultSemiBold" type='defaultSemiBold'
> >
{loading ? "Signing in..." : "Sign in with Microsoft"} {loading ? 'Signing in...' : 'Sign in with Microsoft'}
</ThemedText> </ThemedText>
</ThemedButton> </ThemedButton>
</ThemedView> </ThemedView>

View File

@ -1,3 +1,12 @@
export type updateUser = {
id?: string;
updated_at?: Date;
email?: string;
full_name?: string;
avatar_url?: string;
provider?: string;
};
export type NotificationMessage = { export type NotificationMessage = {
sound?: string; sound?: string;
title: string; title: string;

View File

@ -13,12 +13,15 @@ Notifications.setNotificationHandler({
}), }),
}); });
export const sendPushNotification = async(expoPushToken: string | null, notification: NotificationMessage) => { export const sendPushNotification = async (
expoPushToken: string | null,
notification: NotificationMessage,
) => {
if (!expoPushToken) { if (!expoPushToken) {
Alert.alert('Error', 'No push token found.'); Alert.alert('Error', 'No push token found.');
return; return;
} }
const message = { const message = {
to: expoPushToken, to: expoPushToken,
sound: notification.sound ?? 'default', sound: notification.sound ?? 'default',
title: notification.title, title: notification.title,
@ -86,18 +89,20 @@ async function registerForPushNotificationsAsync() {
const PushNotificationManager = ({ children }: { children: React.ReactNode }) => { const PushNotificationManager = ({ children }: { children: React.ReactNode }) => {
const [expoPushToken, setExpoPushToken] = useState<string | undefined>(''); const [expoPushToken, setExpoPushToken] = useState<string | undefined>('');
const [notification, setNotification] = useState<Notifications.Notification | undefined>(undefined); const [notification, setNotification] = useState<Notifications.Notification | undefined>(
undefined,
);
const notificationListener = useRef<Notifications.Subscription>(); const notificationListener = useRef<Notifications.Subscription>();
const responseListener = useRef<Notifications.Subscription>(); const responseListener = useRef<Notifications.Subscription>();
useEffect(() => { useEffect(() => {
registerForPushNotificationsAsync().then(token => setExpoPushToken(token)); registerForPushNotificationsAsync().then((token) => setExpoPushToken(token));
notificationListener.current = Notifications.addNotificationReceivedListener(notification => { notificationListener.current = Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification); setNotification(notification);
}); });
responseListener.current = Notifications.addNotificationResponseReceivedListener(response => { responseListener.current = Notifications.addNotificationResponseReceivedListener((response) => {
console.log(response); console.log(response);
// Handle notification response here // Handle notification response here
}); });
@ -109,5 +114,5 @@ const PushNotificationManager = ({ children }: { children: React.ReactNode }) =>
}, []); }, []);
return <>{children}</>; return <>{children}</>;
} };
export default PushNotificationManager; export default PushNotificationManager;