clerkauth
implemented, sync with db to be added
This commit is contained in:
1 parent
ca790a7b97
commit
3a58d420d6
49 files changed
+9423
-779
No files matched your search
@@ -0,0 +1,16 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function AuthLayout() {
|
||||
return (
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
contentStyle: { backgroundColor: "#f5f5f5" },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="sign-in" />
|
||||
<Stack.Screen name="sign-up" />
|
||||
<Stack.Screen name="onboarding" />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
} from "react-native";
|
||||
import { useUser, useAuth } from "@clerk/clerk-expo";
|
||||
import { useRouter } from "expo-router";
|
||||
import { fitnessProfileApi } from "@/api/fitnessProfile";
|
||||
|
||||
export default function OnboardingScreen() {
|
||||
const { user } = useUser();
|
||||
const { getToken } = useAuth();
|
||||
const router = useRouter();
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [fitnessProfile, setFitnessProfile] = useState({
|
||||
height: "",
|
||||
weight: "",
|
||||
goals: "",
|
||||
fitnessLevel: "beginner",
|
||||
medicalConditions: "",
|
||||
dietaryRestrictions: "",
|
||||
preferredWorkoutTime: "morning",
|
||||
workoutFrequency: "3",
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!user?.id) return;
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Validate required fields
|
||||
if (!fitnessProfile.height || !fitnessProfile.weight) {
|
||||
Alert.alert("Error", "Please enter your height and weight");
|
||||
return;
|
||||
}
|
||||
|
||||
const fitnessData = {
|
||||
clientId: user.id,
|
||||
height: parseFloat(fitnessProfile.height),
|
||||
weight: parseFloat(fitnessProfile.weight),
|
||||
goals: fitnessProfile.goals || undefined,
|
||||
fitnessLevel: fitnessProfile.fitnessLevel as
|
||||
| "beginner"
|
||||
| "intermediate"
|
||||
| "advanced",
|
||||
medicalConditions: fitnessProfile.medicalConditions || undefined,
|
||||
dietaryRestrictions: fitnessProfile.dietaryRestrictions || undefined,
|
||||
preferredWorkoutTime: fitnessProfile.preferredWorkoutTime as
|
||||
| "morning"
|
||||
| "afternoon"
|
||||
| "evening",
|
||||
workoutFrequency: parseInt(fitnessProfile.workoutFrequency) || 3,
|
||||
};
|
||||
|
||||
const token = await getToken();
|
||||
if (!token) {
|
||||
throw new Error("Authentication token not available");
|
||||
}
|
||||
await fitnessProfileApi.createFitnessProfile(fitnessData, token);
|
||||
router.replace("/(tabs)");
|
||||
} catch (error) {
|
||||
console.error("Error creating fitness profile:", error);
|
||||
Alert.alert(
|
||||
"Error",
|
||||
"Failed to create fitness profile. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLevelSelect = (level: string) => {
|
||||
setFitnessProfile({ ...fitnessProfile, fitnessLevel: level });
|
||||
};
|
||||
|
||||
const handleTimeSelect = (time: string) => {
|
||||
setFitnessProfile({ ...fitnessProfile, preferredWorkoutTime: time });
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container}>
|
||||
<Text style={styles.title}>Set Up Your Fitness Profile</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Help us personalize your fitness journey
|
||||
</Text>
|
||||
|
||||
<View style={styles.form}>
|
||||
<Text style={styles.label}>Height (cm)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={fitnessProfile.height}
|
||||
onChangeText={(value) =>
|
||||
setFitnessProfile({ ...fitnessProfile, height: value })
|
||||
}
|
||||
keyboardType="numeric"
|
||||
placeholder="Enter height in cm"
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Weight (kg)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={fitnessProfile.weight}
|
||||
onChangeText={(value) =>
|
||||
setFitnessProfile({ ...fitnessProfile, weight: value })
|
||||
}
|
||||
keyboardType="numeric"
|
||||
placeholder="Enter weight in kg"
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Fitness Goals</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={fitnessProfile.goals}
|
||||
onChangeText={(value) =>
|
||||
setFitnessProfile({ ...fitnessProfile, goals: value })
|
||||
}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
placeholder="What are your fitness goals?"
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Fitness Level</Text>
|
||||
<View style={styles.buttonGroup}>
|
||||
{["beginner", "intermediate", "advanced"].map((level) => (
|
||||
<TouchableOpacity
|
||||
key={level}
|
||||
style={[
|
||||
styles.levelButton,
|
||||
fitnessProfile.fitnessLevel === level && styles.selectedButton,
|
||||
]}
|
||||
onPress={() => handleLevelSelect(level)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.levelButtonText,
|
||||
fitnessProfile.fitnessLevel === level &&
|
||||
styles.selectedButtonText,
|
||||
]}
|
||||
>
|
||||
{level.charAt(0).toUpperCase() + level.slice(1)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.label}>Medical Conditions</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={fitnessProfile.medicalConditions}
|
||||
onChangeText={(value) =>
|
||||
setFitnessProfile({ ...fitnessProfile, medicalConditions: value })
|
||||
}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
placeholder="Any medical conditions we should know about?"
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Dietary Restrictions</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={fitnessProfile.dietaryRestrictions}
|
||||
onChangeText={(value) =>
|
||||
setFitnessProfile({
|
||||
...fitnessProfile,
|
||||
dietaryRestrictions: value,
|
||||
})
|
||||
}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
placeholder="Any dietary restrictions?"
|
||||
/>
|
||||
|
||||
<Text style={styles.label}>Preferred Workout Time</Text>
|
||||
<View style={styles.buttonGroup}>
|
||||
{["morning", "afternoon", "evening"].map((time) => (
|
||||
<TouchableOpacity
|
||||
key={time}
|
||||
style={[
|
||||
styles.timeButton,
|
||||
fitnessProfile.preferredWorkoutTime === time &&
|
||||
styles.selectedButton,
|
||||
]}
|
||||
onPress={() => handleTimeSelect(time)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.timeButtonText,
|
||||
fitnessProfile.preferredWorkoutTime === time &&
|
||||
styles.selectedButtonText,
|
||||
]}
|
||||
>
|
||||
{time.charAt(0).toUpperCase() + time.slice(1)}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.label}>Workouts per Week</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={fitnessProfile.workoutFrequency}
|
||||
onChangeText={(value) =>
|
||||
setFitnessProfile({ ...fitnessProfile, workoutFrequency: value })
|
||||
}
|
||||
keyboardType="numeric"
|
||||
placeholder="Number of workouts per week"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.submitButton}
|
||||
onPress={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<ActivityIndicator color="white" />
|
||||
) : (
|
||||
<Text style={styles.submitButtonText}>Complete Setup</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginTop: 40,
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
textAlign: "center",
|
||||
marginBottom: 32,
|
||||
},
|
||||
form: {
|
||||
padding: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#374151",
|
||||
marginBottom: 4,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 16,
|
||||
backgroundColor: "white",
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
textAlignVertical: "top",
|
||||
},
|
||||
buttonGroup: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: 16,
|
||||
},
|
||||
levelButton: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f3f4f6",
|
||||
padding: 10,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 4,
|
||||
alignItems: "center",
|
||||
},
|
||||
timeButton: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f3f4f6",
|
||||
padding: 10,
|
||||
borderRadius: 8,
|
||||
marginHorizontal: 4,
|
||||
alignItems: "center",
|
||||
},
|
||||
selectedButton: {
|
||||
backgroundColor: "#3b82f6",
|
||||
},
|
||||
levelButtonText: {
|
||||
color: "#374151",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
timeButtonText: {
|
||||
color: "#374151",
|
||||
fontSize: 14,
|
||||
fontWeight: "500",
|
||||
},
|
||||
selectedButtonText: {
|
||||
color: "white",
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: "#3b82f6",
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
marginTop: 24,
|
||||
},
|
||||
submitButtonText: {
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import React from "react";
|
||||
import { useSignIn, useAuth } from "@clerk/clerk-expo";
|
||||
import { useRouter } from "expo-router";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
|
||||
export default function SignInScreen() {
|
||||
const { signIn, setActive, isLoaded } = useSignIn();
|
||||
const { isSignedIn } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState("");
|
||||
|
||||
// Redirect if already signed in
|
||||
React.useEffect(() => {
|
||||
if (isSignedIn) {
|
||||
router.replace("/(tabs)");
|
||||
}
|
||||
}, [isSignedIn]);
|
||||
|
||||
const onSignInPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const signInAttempt = await signIn.create({
|
||||
identifier: emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
if (signInAttempt.status === "complete") {
|
||||
await setActive({ session: signInAttempt.createdSessionId });
|
||||
router.replace("/(tabs)");
|
||||
} else {
|
||||
console.error(
|
||||
"Sign-in incomplete:",
|
||||
JSON.stringify(signInAttempt, null, 2),
|
||||
);
|
||||
setError("Sign-in incomplete. Please try again.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Sign-in error:", JSON.stringify(err, null, 2));
|
||||
|
||||
// Handle specific error codes
|
||||
if (err.errors?.[0]?.code === "session_exists") {
|
||||
// User is already signed in, just redirect
|
||||
router.replace("/(tabs)");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(
|
||||
err.errors?.[0]?.message ||
|
||||
"Failed to sign in. Please check your credentials.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render the form if already signed in
|
||||
if (isSignedIn) {
|
||||
return (
|
||||
<View style={[styles.container, styles.centerContent]}>
|
||||
<ActivityIndicator size="large" color="#2563eb" />
|
||||
<Text style={styles.loadingText}>Redirecting...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Welcome Back</Text>
|
||||
<Text style={styles.subtitle}>Sign in to continue to FitAI</Text>
|
||||
|
||||
{error ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Enter your email"
|
||||
placeholderTextColor="#999"
|
||||
onChangeText={setEmailAddress}
|
||||
keyboardType="email-address"
|
||||
autoComplete="email"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={password}
|
||||
placeholder="Enter your password"
|
||||
placeholderTextColor="#999"
|
||||
secureTextEntry={true}
|
||||
onChangeText={setPassword}
|
||||
autoComplete="password"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={onSignInPress}
|
||||
disabled={loading || !emailAddress || !password}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Sign In</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Don't have an account? </Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/(auth)/sign-up")}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.linkText}>Sign Up</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
centerContent: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: "bold",
|
||||
color: "#1a1a1a",
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
marginBottom: 32,
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: "#fee",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: "#f44",
|
||||
},
|
||||
errorText: {
|
||||
color: "#c00",
|
||||
fontSize: 14,
|
||||
},
|
||||
form: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#333",
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: "#fff",
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: "#1a1a1a",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#2563eb",
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: "#93c5fd",
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
color: "#666",
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
color: "#2563eb",
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
import React from "react";
|
||||
import { useSignUp, useAuth } from "@clerk/clerk-expo";
|
||||
import { useRouter } from "expo-router";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
|
||||
export default function SignUpScreen() {
|
||||
const { signUp, setActive, isLoaded } = useSignUp();
|
||||
const { isSignedIn } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [firstName, setFirstName] = React.useState("");
|
||||
const [lastName, setLastName] = React.useState("");
|
||||
const [pendingVerification, setPendingVerification] = React.useState(false);
|
||||
const [code, setCode] = React.useState("");
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [error, setError] = React.useState("");
|
||||
|
||||
// Redirect if already signed in
|
||||
React.useEffect(() => {
|
||||
if (isSignedIn) {
|
||||
router.replace("/(tabs)");
|
||||
}
|
||||
}, [isSignedIn]);
|
||||
|
||||
const onSignUpPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
await signUp.create({
|
||||
emailAddress,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
|
||||
// Send email verification code
|
||||
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
|
||||
|
||||
setPendingVerification(true);
|
||||
} catch (err: any) {
|
||||
console.error("Sign-up error:", JSON.stringify(err, null, 2));
|
||||
setError(
|
||||
err.errors?.[0]?.message || "Failed to sign up. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onVerifyPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const completeSignUp = await signUp.attemptEmailAddressVerification({
|
||||
code,
|
||||
});
|
||||
|
||||
if (completeSignUp.status === "complete") {
|
||||
await setActive({ session: completeSignUp.createdSessionId });
|
||||
router.replace("/(tabs)");
|
||||
} else {
|
||||
console.error(
|
||||
"Verification incomplete:",
|
||||
JSON.stringify(completeSignUp, null, 2),
|
||||
);
|
||||
setError("Verification incomplete. Please try again.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Verification error:", JSON.stringify(err, null, 2));
|
||||
|
||||
// Handle specific error codes
|
||||
if (err.errors?.[0]?.code === "session_exists") {
|
||||
// User is already signed in, just redirect
|
||||
router.replace("/(tabs)");
|
||||
return;
|
||||
}
|
||||
|
||||
setError(err.errors?.[0]?.message || "Invalid verification code.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render the form if already signed in
|
||||
if (isSignedIn) {
|
||||
return (
|
||||
<View style={[styles.container, styles.centerContent]}>
|
||||
<ActivityIndicator size="large" color="#2563eb" />
|
||||
<Text style={styles.loadingText}>Redirecting...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (pendingVerification) {
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Verify Email</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Enter the verification code sent to {emailAddress}
|
||||
</Text>
|
||||
|
||||
{error ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Verification Code</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={code}
|
||||
placeholder="Enter verification code"
|
||||
placeholderTextColor="#999"
|
||||
onChangeText={setCode}
|
||||
keyboardType="number-pad"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={onVerifyPress}
|
||||
disabled={loading || !code}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Verify Email</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={styles.container}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Create Account</Text>
|
||||
<Text style={styles.subtitle}>Sign up to get started with FitAI</Text>
|
||||
|
||||
{error ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.form}>
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>First Name</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={firstName}
|
||||
placeholder="Enter your first name"
|
||||
placeholderTextColor="#999"
|
||||
onChangeText={setFirstName}
|
||||
autoComplete="given-name"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Last Name</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={lastName}
|
||||
placeholder="Enter your last name"
|
||||
placeholderTextColor="#999"
|
||||
onChangeText={setLastName}
|
||||
autoComplete="family-name"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Enter your email"
|
||||
placeholderTextColor="#999"
|
||||
onChangeText={setEmailAddress}
|
||||
keyboardType="email-address"
|
||||
autoComplete="email"
|
||||
editable={!loading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.inputContainer}>
|
||||
<Text style={styles.label}>Password</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={password}
|
||||
placeholder="Create a password"
|
||||
placeholderTextColor="#999"
|
||||
secureTextEntry={true}
|
||||
onChangeText={setPassword}
|
||||
autoComplete="password-new"
|
||||
editable={!loading}
|
||||
/>
|
||||
<Text style={styles.hint}>Must be at least 8 characters</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, loading && styles.buttonDisabled]}
|
||||
onPress={onSignUpPress}
|
||||
disabled={
|
||||
loading || !emailAddress || !password || !firstName || !lastName
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>Sign Up</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Already have an account? </Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.push("/(auth)/sign-in")}
|
||||
disabled={loading}
|
||||
>
|
||||
<Text style={styles.linkText}>Sign In</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#f5f5f5",
|
||||
},
|
||||
centerContent: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 12,
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 40,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: "bold",
|
||||
color: "#1a1a1a",
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: "#666",
|
||||
marginBottom: 32,
|
||||
},
|
||||
errorContainer: {
|
||||
backgroundColor: "#fee",
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: "#f44",
|
||||
},
|
||||
errorText: {
|
||||
color: "#c00",
|
||||
fontSize: 14,
|
||||
},
|
||||
form: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: "600",
|
||||
color: "#333",
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: "#fff",
|
||||
borderWidth: 1,
|
||||
borderColor: "#ddd",
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
fontSize: 16,
|
||||
color: "#1a1a1a",
|
||||
},
|
||||
hint: {
|
||||
fontSize: 12,
|
||||
color: "#999",
|
||||
marginTop: 4,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: "#2563eb",
|
||||
paddingVertical: 16,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: "#93c5fd",
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
color: "#666",
|
||||
},
|
||||
linkText: {
|
||||
fontSize: 14,
|
||||
color: "#2563eb",
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user