485 lines
No EOL
14 KiB
TypeScript
485 lines
No EOL
14 KiB
TypeScript
import React, { useState } from "react";
|
|
import { View, TextInput, Alert } from "react-native";
|
|
import { useForm, Controller } from "react-hook-form";
|
|
|
|
import { MyButton, MyText, MyTextInput, ProfileImage, tw, BottomDialog } from "common-ui";
|
|
import { trpc } from "@/src/trpc-client";
|
|
|
|
interface RegisterFormInputs {
|
|
name: string;
|
|
email: string;
|
|
mobile: string;
|
|
password: string;
|
|
confirmPassword: string;
|
|
termsAccepted: boolean;
|
|
profileImageUri?: string;
|
|
}
|
|
|
|
interface RegistrationFormProps {
|
|
onSubmit: (data: FormData) => void | Promise<void>;
|
|
isLoading?: boolean;
|
|
initialValues?: Partial<RegisterFormInputs>;
|
|
isEdit?: boolean;
|
|
}
|
|
|
|
function RegistrationForm({ onSubmit, isLoading = false, initialValues, isEdit = false }: RegistrationFormProps) {
|
|
const [profileImageUri, setProfileImageUri] = useState<string | undefined>();
|
|
const [profileImageFile, setProfileImageFile] = useState<any>();
|
|
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const updatePasswordMutation = trpc.user.auth.updatePassword.useMutation();
|
|
|
|
// Set initial profile image URI for edit mode
|
|
React.useEffect(() => {
|
|
if (isEdit && initialValues?.profileImageUri) {
|
|
setProfileImageUri(initialValues.profileImageUri);
|
|
}
|
|
}, [isEdit, initialValues?.profileImageUri]);
|
|
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
setError,
|
|
clearErrors,
|
|
watch,
|
|
} = useForm<RegisterFormInputs>({
|
|
defaultValues: {
|
|
name: "",
|
|
email: "",
|
|
mobile: "",
|
|
password: "",
|
|
confirmPassword: "",
|
|
termsAccepted: false,
|
|
...initialValues,
|
|
},
|
|
});
|
|
|
|
|
|
|
|
const validateMobile = (mobile: string): boolean => {
|
|
// Remove all non-digit characters
|
|
const cleanMobile = mobile.replace(/\D/g, '');
|
|
// Check if it's a valid Indian mobile number (10 digits, starts with 6-9)
|
|
return cleanMobile.length === 10 && /^[6-9]/.test(cleanMobile);
|
|
};
|
|
|
|
const validateEmail = (email: string): boolean => {
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
return emailRegex.test(email);
|
|
};
|
|
|
|
const handleFormSubmit = async (data: RegisterFormInputs) => {
|
|
clearErrors();
|
|
|
|
// Validate name
|
|
if (!data.name.trim()) {
|
|
setError("name", {
|
|
type: "manual",
|
|
message: "Name is required",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (data.name.trim().length < 2) {
|
|
setError("name", {
|
|
type: "manual",
|
|
message: "Name must be at least 2 characters",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate email
|
|
if (!data.email.trim()) {
|
|
setError("email", {
|
|
type: "manual",
|
|
message: "Email is required",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!validateEmail(data.email)) {
|
|
setError("email", {
|
|
type: "manual",
|
|
message: "Please enter a valid email address",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate mobile number
|
|
if (!data.mobile.trim()) {
|
|
setError("mobile", {
|
|
type: "manual",
|
|
message: "Mobile number is required",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!validateMobile(data.mobile)) {
|
|
setError("mobile", {
|
|
type: "manual",
|
|
message: "Please enter a valid 10-digit mobile number",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate password (only in registration mode)
|
|
if (!isEdit) {
|
|
if (!data.password) {
|
|
setError("password", {
|
|
type: "manual",
|
|
message: "Password is required",
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (data.password.length < 6) {
|
|
setError("password", {
|
|
type: "manual",
|
|
message: "Password must be at least 6 characters",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Validate confirm password
|
|
if (data.password !== data.confirmPassword) {
|
|
setError("confirmPassword", {
|
|
type: "manual",
|
|
message: "Passwords do not match",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate terms (only in registration mode)
|
|
if (!isEdit && !data.termsAccepted) {
|
|
setError("termsAccepted", {
|
|
type: "manual",
|
|
message: "You must accept the terms and conditions",
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Create FormData
|
|
const formData = new FormData();
|
|
formData.append('name', data.name.trim());
|
|
formData.append('email', data.email.trim().toLowerCase());
|
|
formData.append('mobile', data.mobile.replace(/\D/g, ''));
|
|
|
|
// Only include password if provided (for edit mode)
|
|
if (data.password) {
|
|
formData.append('password', data.password);
|
|
}
|
|
|
|
if (profileImageFile) {
|
|
|
|
formData.append('profileImage', {
|
|
uri: profileImageFile.uri,
|
|
type: profileImageFile.mimeType || 'image/jpeg',
|
|
name: profileImageFile.name || 'profile.jpg',
|
|
} as any);
|
|
}
|
|
|
|
await onSubmit(formData);
|
|
};
|
|
|
|
const handleUpdatePassword = async () => {
|
|
if (password !== confirmPassword) {
|
|
Alert.alert('Error', 'Passwords do not match');
|
|
return;
|
|
}
|
|
if (password.length < 6) {
|
|
Alert.alert('Error', 'Password must be at least 6 characters');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await updatePasswordMutation.mutateAsync({ password });
|
|
Alert.alert('Success', 'Password updated successfully');
|
|
setIsPasswordDialogOpen(false);
|
|
setPassword('');
|
|
setConfirmPassword('');
|
|
} catch (error: any) {
|
|
Alert.alert('Error', error.message || 'Failed to update password');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<View style={tw`bg-white rounded-xl p-6 shadow-md mb-6`}>
|
|
<View style={tw`items-center mb-6`}>
|
|
<ProfileImage
|
|
imageUri={profileImageUri}
|
|
onImageSelect={(uri, file) => {
|
|
setProfileImageUri(uri);
|
|
setProfileImageFile(file);
|
|
}}
|
|
size={100}
|
|
editable={true}
|
|
/>
|
|
</View>
|
|
<Controller
|
|
control={control}
|
|
name="name"
|
|
rules={{
|
|
required: "Name is required",
|
|
}}
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<View style={tw`mb-5`}>
|
|
<MyTextInput
|
|
topLabel="Full Name"
|
|
placeholder="Enter your full name"
|
|
value={value}
|
|
onChangeText={onChange}
|
|
onBlur={onBlur}
|
|
style={tw`bg-gray-50`}
|
|
error={!!errors.name}
|
|
/>
|
|
</View>
|
|
)}
|
|
/>
|
|
{errors.name && (
|
|
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
|
|
{errors.name.message}
|
|
</MyText>
|
|
)}
|
|
|
|
<Controller
|
|
control={control}
|
|
name="email"
|
|
rules={{
|
|
required: "Email is required",
|
|
}}
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<View style={tw`mb-5`}>
|
|
<MyTextInput
|
|
topLabel="Email Address"
|
|
placeholder="Enter your email address"
|
|
value={value}
|
|
onChangeText={onChange}
|
|
onBlur={onBlur}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
style={tw`bg-gray-50`}
|
|
error={!!errors.email}
|
|
/>
|
|
</View>
|
|
)}
|
|
/>
|
|
{errors.email && (
|
|
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
|
|
{errors.email.message}
|
|
</MyText>
|
|
)}
|
|
|
|
<Controller
|
|
control={control}
|
|
name="mobile"
|
|
rules={{
|
|
required: "Mobile number is required",
|
|
}}
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<View style={tw`mb-5`}>
|
|
<MyTextInput
|
|
topLabel="Mobile Number"
|
|
placeholder="Enter your mobile number"
|
|
value={value}
|
|
onChangeText={(text) => {
|
|
// Format mobile number as user types
|
|
const clean = text.replace(/\D/g, '');
|
|
if (clean.length <= 10) {
|
|
onChange(clean);
|
|
}
|
|
}}
|
|
onBlur={onBlur}
|
|
keyboardType="phone-pad"
|
|
maxLength={10}
|
|
style={tw`bg-gray-50`}
|
|
error={!!errors.mobile}
|
|
/>
|
|
</View>
|
|
)}
|
|
/>
|
|
{errors.mobile && (
|
|
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
|
|
{errors.mobile.message}
|
|
</MyText>
|
|
)}
|
|
|
|
{!isEdit && (
|
|
<>
|
|
<Controller
|
|
control={control}
|
|
name="password"
|
|
rules={{ required: "Password is required" }}
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<View style={tw`mb-5`}>
|
|
<MyTextInput
|
|
topLabel="Password"
|
|
placeholder="Create a password"
|
|
value={value}
|
|
onChangeText={onChange}
|
|
onBlur={onBlur}
|
|
secureTextEntry
|
|
style={tw`bg-gray-50`}
|
|
error={!!errors.password}
|
|
/>
|
|
</View>
|
|
)}
|
|
/>
|
|
{errors.password && (
|
|
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
|
|
{errors.password.message}
|
|
</MyText>
|
|
)}
|
|
|
|
<Controller
|
|
control={control}
|
|
name="confirmPassword"
|
|
rules={{ required: "Please confirm your password" }}
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<View style={tw`mb-5`}>
|
|
<MyTextInput
|
|
topLabel="Confirm Password"
|
|
placeholder="Confirm your password"
|
|
value={value}
|
|
onChangeText={onChange}
|
|
onBlur={onBlur}
|
|
secureTextEntry
|
|
style={tw`bg-gray-50`}
|
|
error={!!errors.confirmPassword}
|
|
/>
|
|
</View>
|
|
)}
|
|
/>
|
|
{errors.confirmPassword && (
|
|
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
|
|
{errors.confirmPassword.message}
|
|
</MyText>
|
|
)}
|
|
|
|
<Controller
|
|
control={control}
|
|
name="termsAccepted"
|
|
rules={{ required: "You must accept the terms and conditions" }}
|
|
render={({ field: { onChange, value } }) => (
|
|
<View style={tw`mb-6`}>
|
|
<MyTouchableOpacity
|
|
style={tw`flex-row items-center`}
|
|
onPress={() => onChange(!value)}
|
|
>
|
|
<View
|
|
style={tw`w-5 h-5 border-2 border-gray-300 rounded mr-3 ${
|
|
value ? 'bg-blue-600 border-blue-600' : 'bg-white'
|
|
}`}
|
|
>
|
|
{value && (
|
|
<MyText style={tw`text-white text-xs font-bold text-center mt-0.5`}>
|
|
✓
|
|
</MyText>
|
|
)}
|
|
</View>
|
|
<MyText style={tw`text-sm text-gray-600 flex-1`}>
|
|
I agree to the{" "}
|
|
<MyText weight="semibold" style={tw`text-blue-600`}>
|
|
Terms and Conditions
|
|
</MyText>{" "}
|
|
and{" "}
|
|
<MyText weight="semibold" style={tw`text-blue-600`}>
|
|
Privacy Policy
|
|
</MyText>
|
|
</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
)}
|
|
/>
|
|
{errors.termsAccepted && (
|
|
<MyText style={tw`text-red-500 text-sm mb-4 text-center`}>
|
|
{errors.termsAccepted.message}
|
|
</MyText>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<MyButton
|
|
onPress={handleSubmit(handleFormSubmit)}
|
|
fillColor="brand500"
|
|
textColor="white1"
|
|
fullWidth
|
|
disabled={isLoading}
|
|
style={tw` rounded-lg`}
|
|
>
|
|
{isLoading ? (isEdit ? "Updating..." : "Creating Account...") : (isEdit ? "Update Profile" : "Create Account")}
|
|
</MyButton>
|
|
|
|
{isEdit && (
|
|
<View style={tw`mt-4`}>
|
|
<MyButton
|
|
textContent="Update Password"
|
|
onPress={() => setIsPasswordDialogOpen(true)}
|
|
fillColor="brand500"
|
|
textColor="white1"
|
|
fullWidth
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{isEdit && (
|
|
<BottomDialog open={isPasswordDialogOpen} onClose={() => setIsPasswordDialogOpen(false)}>
|
|
<View style={{ padding: 20 }}>
|
|
<MyText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 16 }}>
|
|
Update Password
|
|
</MyText>
|
|
<TextInput
|
|
style={{
|
|
borderWidth: 1,
|
|
borderColor: '#ccc',
|
|
borderRadius: 8,
|
|
padding: 10,
|
|
marginBottom: 12,
|
|
fontSize: 16,
|
|
}}
|
|
placeholder="New Password"
|
|
secureTextEntry
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
/>
|
|
<TextInput
|
|
style={{
|
|
borderWidth: 1,
|
|
borderColor: '#ccc',
|
|
borderRadius: 8,
|
|
padding: 10,
|
|
marginBottom: 20,
|
|
fontSize: 16,
|
|
}}
|
|
placeholder="Confirm Password"
|
|
secureTextEntry
|
|
value={confirmPassword}
|
|
onChangeText={setConfirmPassword}
|
|
/>
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
|
<MyButton
|
|
textContent="Cancel"
|
|
onPress={() => setIsPasswordDialogOpen(false)}
|
|
fillColor="gray1"
|
|
textColor="white1"
|
|
/>
|
|
<MyButton
|
|
textContent="Update"
|
|
onPress={handleUpdatePassword}
|
|
fillColor="brand500"
|
|
textColor="white1"
|
|
disabled={updatePasswordMutation.isPending}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</BottomDialog>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default RegistrationForm; |