Add Sign in with Apple sorta
This commit is contained in:
129
components/auth/AppleSignIn.tsx
Normal file
129
components/auth/AppleSignIn.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { StyleSheet, Alert, Platform } from 'react-native';
|
||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
|
||||
type AppleSignInProps = {
|
||||
onSignInStart?: () => void;
|
||||
onSignInComplete?: () => void;
|
||||
onSignInError?: (error: any) => void;
|
||||
};
|
||||
|
||||
const AppleSignIn: React.FC<AppleSignInProps> = ({
|
||||
onSignInStart,
|
||||
onSignInComplete,
|
||||
onSignInError,
|
||||
}) => {
|
||||
const scheme = useColorScheme() ?? 'dark';
|
||||
const [isAppleAuthAvailable, setIsAppleAuthAvailable] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'ios') {
|
||||
AppleAuthentication.isAvailableAsync().then(setIsAppleAuthAvailable);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAppleSignIn = async () => {
|
||||
try {
|
||||
onSignInStart?.();
|
||||
|
||||
// Get credentials from Apple
|
||||
const credential = await AppleAuthentication.signInAsync({
|
||||
requestedScopes: [
|
||||
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
|
||||
AppleAuthentication.AppleAuthenticationScope.EMAIL,
|
||||
],
|
||||
});
|
||||
|
||||
if (!credential.email) {
|
||||
throw new Error('Email is required for Apple Sign In');
|
||||
}
|
||||
|
||||
// Extract user information
|
||||
const { email, fullName, user: appleUserId } = credential;
|
||||
|
||||
// Create a name from the fullName object if available
|
||||
let name = null;
|
||||
if (fullName?.givenName || fullName?.familyName) {
|
||||
name = `${fullName?.givenName || ''} ${fullName?.familyName || ''}`.trim();
|
||||
}
|
||||
|
||||
// Create a deterministic password based on the Apple user ID
|
||||
// This way the user can sign in again with the same password
|
||||
const password = `Apple-${appleUserId.substring(0, 16)}`;
|
||||
|
||||
// First try to sign in (in case the user already exists)
|
||||
const { data: signInData, error: signInError } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (!signInError && signInData?.user) {
|
||||
// User exists and signed in successfully
|
||||
onSignInComplete?.();
|
||||
return;
|
||||
}
|
||||
|
||||
// If sign-in failed, create a new user
|
||||
const { data: signUpData, error: signUpError } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
full_name: name,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (signUpError) {
|
||||
throw signUpError;
|
||||
}
|
||||
|
||||
// User created successfully
|
||||
onSignInComplete?.();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Apple sign in error:', error);
|
||||
|
||||
if (error.code === 'ERR_REQUEST_CANCELED') {
|
||||
console.log('Sign in was canceled');
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Sign in error',
|
||||
'An error occurred while signing in with Apple. Please try again.'
|
||||
);
|
||||
onSignInError?.(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Only render on iOS and if Apple Authentication is available
|
||||
if (Platform.OS !== 'ios' || !isAppleAuthAvailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AppleAuthentication.AppleAuthenticationButton
|
||||
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
|
||||
buttonStyle={
|
||||
scheme === 'light'
|
||||
? AppleAuthentication.AppleAuthenticationButtonStyle.BLACK
|
||||
: AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
|
||||
}
|
||||
cornerRadius={10}
|
||||
style={styles.button}
|
||||
onPress={handleAppleSignIn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
width: 320,
|
||||
height: 50,
|
||||
marginVertical: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default AppleSignIn;
|
162
components/auth/Login.tsx
Normal file
162
components/auth/Login.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Alert, StyleSheet, AppState, Image, Platform } from 'react-native';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
|
||||
import AppleSignIn from '@/components/auth/AppleSignIn';
|
||||
|
||||
// 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
|
||||
// `onAuthStateChange` events with the `TOKEN_REFRESHED` or `SIGNED_OUT` event
|
||||
// if the user's session is terminated. This should only be registered once.
|
||||
if (Platform.OS !== 'web') {
|
||||
AppState.addEventListener('change', (state) => {
|
||||
if (state === 'active') {
|
||||
supabase.auth.startAutoRefresh();
|
||||
} else {
|
||||
supabase.auth.stopAutoRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const LoginPage = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Set up auto-refreshing for web
|
||||
useEffect(() => {
|
||||
if (Platform.OS === 'web') {
|
||||
supabase.auth.startAutoRefresh();
|
||||
return () => {
|
||||
supabase.auth.stopAutoRefresh();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const signInWithEmail = async () => {
|
||||
setLoading(true);
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
if (error) Alert.alert(error.message);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const signUpWithEmail = async () => {
|
||||
setLoading(true);
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await supabase.auth.signUp({
|
||||
email: email,
|
||||
password: password,
|
||||
});
|
||||
if (error) Alert.alert(error.message);
|
||||
else if (!session) Alert.alert('Please check your inbox for email verification!');
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<ThemedView style={styles.titleContainer}>
|
||||
<Image source={require('@/assets/images/tech_tracker_logo.png')} style={styles.reactLogo} />
|
||||
<ThemedText type='title' style={styles.headerTitle}>
|
||||
Tech Tracker
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
<ThemedView style={[styles.verticallySpaced]}>
|
||||
<ThemedTextInput
|
||||
fontSize={24}
|
||||
onChangeText={(text) => setEmail(text)}
|
||||
value={email}
|
||||
placeholder='email@address.com'
|
||||
/>
|
||||
</ThemedView>
|
||||
|
||||
<ThemedView style={styles.verticallySpaced}>
|
||||
<ThemedTextInput
|
||||
fontSize={24}
|
||||
onChangeText={(text) => setPassword(text)}
|
||||
value={password}
|
||||
secureTextEntry={true}
|
||||
placeholder='Password'
|
||||
/>
|
||||
</ThemedView>
|
||||
|
||||
<ThemedView style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<ThemedTextButton
|
||||
text='Sign in'
|
||||
disabled={loading}
|
||||
onPress={() => signInWithEmail()}
|
||||
fontSize={24}
|
||||
/>
|
||||
</ThemedView>
|
||||
|
||||
<ThemedView style={styles.verticallySpaced}>
|
||||
<ThemedTextButton
|
||||
text='Sign up'
|
||||
disabled={loading}
|
||||
onPress={() => signUpWithEmail()}
|
||||
fontSize={24}
|
||||
/>
|
||||
</ThemedView>
|
||||
|
||||
{/* Apple Sign In - Only shows on iOS */}
|
||||
<ThemedView style={[styles.verticallySpaced, styles.mt20]}>
|
||||
<AppleSignIn
|
||||
onSignInStart={() => setLoading(true)}
|
||||
onSignInComplete={() => setLoading(false)}
|
||||
onSignInError={() => setLoading(false)}
|
||||
/>
|
||||
</ThemedView>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 12,
|
||||
height: '100%',
|
||||
},
|
||||
verticallySpaced: {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
alignItems: 'center',
|
||||
},
|
||||
mt20: {
|
||||
marginTop: 20,
|
||||
},
|
||||
reactLogo: {
|
||||
height: 70,
|
||||
width: 72,
|
||||
},
|
||||
titleContainer: {
|
||||
marginTop: 60,
|
||||
marginBottom: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
headerTitle: {
|
||||
textAlign: 'center',
|
||||
fontSize: 48,
|
||||
lineHeight: 64,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
divider: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginVertical: 20,
|
||||
},
|
||||
dividerText: {
|
||||
marginHorizontal: 10,
|
||||
fontSize: 16,
|
||||
opacity: 0.7,
|
||||
},
|
||||
});
|
23
components/auth/Logout_Button.tsx
Normal file
23
components/auth/Logout_Button.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
|
||||
import { Alert, StyleSheet, AppState } from 'react-native';
|
||||
|
||||
const Logout_Button = () => {
|
||||
const signOut = async () => {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
if (error) Alert.alert(error.message);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemedTextButton
|
||||
width={120}
|
||||
height={60}
|
||||
text='Logout'
|
||||
fontSize={16}
|
||||
onPress={() => signOut()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default Logout_Button;
|
||||
|
||||
const styles = StyleSheet.create({});
|
Reference in New Issue
Block a user