project setup
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { Button } from "react-native-paper";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface AuthButtonProps {
|
||||
onPress: () => void;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
textColor?: string;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AuthButton = ({
|
||||
onPress,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
children,
|
||||
}: AuthButtonProps) => {
|
||||
return (
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={onPress}
|
||||
className="py-2 rounded-lg"
|
||||
buttonColor="#6750a4"
|
||||
disabled={disabled || loading}
|
||||
loading={loading}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Surface } from "react-native-paper";
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
interface AuthCardProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
export const AuthCard = ({ children, title, subtitle }: AuthCardProps) => {
|
||||
return (
|
||||
<Surface
|
||||
style={{
|
||||
borderRadius: 20,
|
||||
padding: 24,
|
||||
backgroundColor: "rgba(255,255,255,0.9)",
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
{(title || subtitle) && (
|
||||
<View style={{ alignItems: "center", marginBottom: 24 }}>
|
||||
{title && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: "bold",
|
||||
color: "#6750a4",
|
||||
marginBottom: subtitle ? 12 : 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
)}
|
||||
{subtitle && (
|
||||
<Text style={{ fontSize: 16, color: "rgba(0,0,0,0.6)" }}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{children}
|
||||
</Surface>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { View, Text, TouchableOpacity } from "react-native";
|
||||
|
||||
interface AuthFooterProps {
|
||||
question: string;
|
||||
actionText: string;
|
||||
onPress: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const AuthFooter = ({
|
||||
question,
|
||||
actionText,
|
||||
onPress,
|
||||
disabled,
|
||||
}: AuthFooterProps) => {
|
||||
return (
|
||||
<View className="flex-row items-center justify-center mt-6">
|
||||
<Text className="text-white">{question}</Text>
|
||||
<TouchableOpacity onPress={onPress} disabled={disabled}>
|
||||
<Text className="text-white font-bold ml-2">{actionText}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { View, Text, Image } from "react-native";
|
||||
|
||||
interface AuthHeaderProps {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const AuthHeader = ({ title = "Project Athena" }: AuthHeaderProps) => {
|
||||
return (
|
||||
<View style={{ alignItems: "center", marginBottom: 36 }}>
|
||||
<Image
|
||||
source={require("../../assets/images/logo_no_background.png")}
|
||||
style={{ width: 80, height: 80, marginBottom: 16 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 28,
|
||||
fontWeight: "bold",
|
||||
color: "white",
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
View,
|
||||
ScrollView,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
} from "react-native";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
|
||||
interface AuthLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthLayout = ({ children }: AuthLayoutProps) => {
|
||||
const { width } = useWindowDimensions();
|
||||
const isWideScreen = width > 768;
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<StatusBar style="light" />
|
||||
<LinearGradient
|
||||
colors={["#A78FA1", "#6750a4", "#49385D"]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: isWideScreen ? 32 : 16,
|
||||
paddingVertical: isWideScreen ? 32 : 24,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: isWideScreen ? "center" : "stretch",
|
||||
width: isWideScreen ? 550 : "100%",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</LinearGradient>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { TouchableOpacity, Text } from "react-native";
|
||||
|
||||
interface ForgotPasswordLinkProps {
|
||||
onPress: () => void;
|
||||
isResetting: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const ForgotPasswordLink = ({
|
||||
onPress,
|
||||
isResetting,
|
||||
disabled,
|
||||
}: ForgotPasswordLinkProps) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="items-center mt-4"
|
||||
onPress={onPress}
|
||||
disabled={disabled || isResetting}
|
||||
>
|
||||
<Text className="text-primaryPurple">
|
||||
{isResetting ? "Sending reset email..." : "Forgot Password?"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { TextInput, TextInputProps } from "react-native-paper";
|
||||
import {
|
||||
KeyboardTypeOptions,
|
||||
Platform,
|
||||
NativeSyntheticEvent,
|
||||
TextInputSubmitEditingEventData,
|
||||
SubmitBehavior,
|
||||
} from "react-native";
|
||||
|
||||
interface FormInputProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onBlur?: () => void;
|
||||
icon?: string;
|
||||
keyboardType?: KeyboardTypeOptions;
|
||||
disabled?: boolean;
|
||||
autoCapitalize?: TextInputProps["autoCapitalize"];
|
||||
style?: any;
|
||||
onSubmitEditing?: (
|
||||
e?: NativeSyntheticEvent<TextInputSubmitEditingEventData>
|
||||
) => void;
|
||||
returnKeyType?: any;
|
||||
submitBehavior?: SubmitBehavior;
|
||||
}
|
||||
|
||||
export const FormInput = ({
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
onBlur,
|
||||
icon = "pencil",
|
||||
keyboardType = "default",
|
||||
disabled = false,
|
||||
autoCapitalize = "none",
|
||||
style = {},
|
||||
onSubmitEditing,
|
||||
returnKeyType,
|
||||
submitBehavior,
|
||||
}: FormInputProps) => {
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
mode="flat"
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
onBlur={onBlur}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
backgroundColor: "transparent",
|
||||
...style,
|
||||
}}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
returnKeyType={returnKeyType}
|
||||
submitBehavior={submitBehavior ?? "submit"}
|
||||
onKeyPress={(e) => {
|
||||
if (Platform.OS === "web" && e.nativeEvent.key === "Enter") {
|
||||
onSubmitEditing?.();
|
||||
}
|
||||
}}
|
||||
left={<TextInput.Icon icon={icon} color="#6750a4" />}
|
||||
theme={{ colors: { primary: "#6750a4" } }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { View, Text } from "react-native";
|
||||
|
||||
interface MessageDisplayProps {
|
||||
message: string;
|
||||
type: "error" | "success";
|
||||
}
|
||||
|
||||
export const MessageDisplay = ({ message, type }: MessageDisplayProps) => {
|
||||
if (!message) return null;
|
||||
|
||||
const backgroundColor =
|
||||
type === "error" ? "rgba(255,0,0,0.1)" : "rgba(0,255,0,0.1)";
|
||||
|
||||
const textColor = type === "error" ? "#F83434" : "#22C927";
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
padding: 10,
|
||||
backgroundColor,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: textColor }}>{message}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
NativeSyntheticEvent,
|
||||
TextInputSubmitEditingEventData,
|
||||
} from "react-native";
|
||||
import { TextInput, TextInputProps } from "react-native-paper";
|
||||
|
||||
interface PasswordInputProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onBlur?: () => void;
|
||||
disabled?: boolean;
|
||||
style?: any;
|
||||
onSubmitEditing?: (
|
||||
e?: NativeSyntheticEvent<TextInputSubmitEditingEventData>
|
||||
) => void;
|
||||
returnKeyType?: any;
|
||||
submitBehavior?: TextInputProps["submitBehavior"];
|
||||
}
|
||||
|
||||
export const PasswordInput = ({
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
onBlur,
|
||||
disabled = false,
|
||||
style = {},
|
||||
onSubmitEditing,
|
||||
returnKeyType,
|
||||
submitBehavior,
|
||||
}: PasswordInputProps) => {
|
||||
const [secureTextEntry, setSecureTextEntry] = useState(true);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
label={label}
|
||||
mode="flat"
|
||||
value={value}
|
||||
secureTextEntry={secureTextEntry}
|
||||
onChangeText={onChangeText}
|
||||
onBlur={onBlur}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
backgroundColor: "transparent",
|
||||
...style,
|
||||
}}
|
||||
onSubmitEditing={onSubmitEditing}
|
||||
returnKeyType={returnKeyType}
|
||||
submitBehavior={submitBehavior ?? "submit"}
|
||||
onKeyPress={(e) => {
|
||||
if (Platform.OS === "web" && e?.nativeEvent?.key === "Enter") {
|
||||
onSubmitEditing?.();
|
||||
}
|
||||
}}
|
||||
left={<TextInput.Icon icon="lock" color="#6750a4" />}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon={secureTextEntry ? "eye" : "eye-off"}
|
||||
color="#6750a4"
|
||||
onPress={() => setSecureTextEntry(!secureTextEntry)}
|
||||
/>
|
||||
}
|
||||
theme={{ colors: { primary: "#6750a4" } }}
|
||||
disabled={disabled}
|
||||
placeholderTextColor="#d9d9d9"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Text } from "react-native";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
import { FormInput } from "@/components/auth/FormInput";
|
||||
import { PasswordInput } from "@/components/auth/PasswordInput";
|
||||
import { MessageDisplay } from "@/components/auth/MessageDisplay";
|
||||
import { AuthButton } from "@/components/auth/AuthButton";
|
||||
|
||||
const signInSchema = z.object({
|
||||
emailAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Email is required")
|
||||
.pipe(z.email("Enter a valid email address")),
|
||||
password: z
|
||||
.string()
|
||||
.min(1, "Password is required")
|
||||
.max(128, "Password is too long"),
|
||||
});
|
||||
|
||||
type SignInValues = z.infer<typeof signInSchema>;
|
||||
|
||||
interface SignInFormProps {
|
||||
errorMessage: string;
|
||||
successMessage: string;
|
||||
isLoading: boolean;
|
||||
onSignInSubmit: (values: {
|
||||
emailAddress: string;
|
||||
password: string;
|
||||
}) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export const SignInForm = ({
|
||||
errorMessage,
|
||||
successMessage,
|
||||
isLoading,
|
||||
onSignInSubmit,
|
||||
}: SignInFormProps) => {
|
||||
const { control, handleSubmit } = useForm<SignInValues>({
|
||||
resolver: zodResolver(signInSchema),
|
||||
defaultValues: { emailAddress: "", password: "" },
|
||||
mode: "onBlur",
|
||||
reValidateMode: "onBlur",
|
||||
shouldUnregister: false,
|
||||
});
|
||||
|
||||
const submit = (vals: SignInValues) =>
|
||||
onSignInSubmit({
|
||||
emailAddress: vals.emailAddress.trim().toLowerCase(),
|
||||
password: vals.password,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={control}
|
||||
name="emailAddress"
|
||||
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
|
||||
<>
|
||||
<FormInput
|
||||
label="Email"
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlur}
|
||||
icon="email"
|
||||
keyboardType="email-address"
|
||||
disabled={isLoading}
|
||||
style={{ marginBottom: 8 }}
|
||||
returnKeyType="next"
|
||||
/>
|
||||
{error ? (
|
||||
<Text style={{ color: "#F83434", marginBottom: 8 }}>
|
||||
{error.message}
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="password"
|
||||
render={({ field: { onChange, onBlur, value }, fieldState: { error } }) => (
|
||||
<>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
onBlur={onBlur}
|
||||
disabled={isLoading}
|
||||
style={{ marginBottom: 8 }}
|
||||
returnKeyType="done"
|
||||
submitBehavior="blurAndSubmit"
|
||||
onSubmitEditing={() => handleSubmit(submit)()}
|
||||
/>
|
||||
{error ? (
|
||||
<Text style={{ color: "#F83434", marginBottom: 8 }}>
|
||||
{error.message}
|
||||
</Text>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
{errorMessage ? (
|
||||
<MessageDisplay message={errorMessage} type="error" />
|
||||
) : null}
|
||||
{successMessage ? (
|
||||
<MessageDisplay message={successMessage} type="success" />
|
||||
) : null}
|
||||
|
||||
<AuthButton
|
||||
onPress={handleSubmit(submit)}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing in..." : "Sign In"}
|
||||
</AuthButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Text, Animated, Pressable } from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
|
||||
import { useAuthContext } from "@/auth/AuthContext";
|
||||
|
||||
interface SignOutButtonProps {
|
||||
textOpacity: Animated.Value;
|
||||
iconTranslateX: Animated.Value;
|
||||
paddingLeft: Animated.Value;
|
||||
}
|
||||
|
||||
export const SignOutButton = ({
|
||||
textOpacity,
|
||||
iconTranslateX,
|
||||
paddingLeft,
|
||||
}: SignOutButtonProps) => {
|
||||
const router = useRouter();
|
||||
const { logout } = useAuthContext();
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await logout();
|
||||
router.replace("/sign-in");
|
||||
} catch (err) {
|
||||
console.error("Logout error:", err);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
paddingLeft,
|
||||
paddingRight: 4,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={handleSignOut}
|
||||
className="flex-row items-center py-3 rounded-lg w-full justify-start"
|
||||
>
|
||||
<Animated.View
|
||||
style={{
|
||||
width: 32,
|
||||
alignItems: "center",
|
||||
transform: [{ translateX: iconTranslateX }],
|
||||
}}
|
||||
>
|
||||
<Ionicons name="log-out-outline" size={32} color="white" />
|
||||
</Animated.View>
|
||||
<Animated.View
|
||||
style={{
|
||||
opacity: textOpacity,
|
||||
marginLeft: 12,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Text className="text-white" numberOfLines={1}>
|
||||
Sign out
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { View, TouchableOpacity } from "react-native";
|
||||
import {
|
||||
Avatar,
|
||||
Divider,
|
||||
Menu,
|
||||
Text,
|
||||
Chip,
|
||||
IconButton,
|
||||
} from "react-native-paper";
|
||||
|
||||
import { useAuthContext } from "@/auth/AuthContext";
|
||||
import { getInitials, getRoleLabel } from "@/utils/userUtils";
|
||||
|
||||
export const UserButton = () => {
|
||||
const { user, logout } = useAuthContext();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const initials = useMemo(
|
||||
() => getInitials(user?.firstName, user?.lastName),
|
||||
[user?.firstName, user?.lastName]
|
||||
);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center">
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={() => setVisible(false)}
|
||||
anchor={
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={() => setVisible(true)}
|
||||
className="ml-3"
|
||||
>
|
||||
{user.imageUrl ? (
|
||||
<Avatar.Image size={32} source={{ uri: user.imageUrl }} />
|
||||
) : (
|
||||
<Avatar.Text size={32} label={initials} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
}
|
||||
contentStyle={{
|
||||
minWidth: 300,
|
||||
borderRadius: 12,
|
||||
overflow: "hidden",
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
backgroundColor: "#F7F7F8",
|
||||
}}
|
||||
>
|
||||
<View style={{ flexDirection: "row", alignItems: "center" }}>
|
||||
{user.imageUrl ? (
|
||||
<Avatar.Image size={44} source={{ uri: user.imageUrl }} />
|
||||
) : (
|
||||
<Avatar.Text size={44} label={initials} />
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
maxWidth: 500,
|
||||
marginLeft: 8,
|
||||
}}
|
||||
>
|
||||
<Text variant="titleSmall" numberOfLines={10}>
|
||||
{user.firstName} {user.lastName}
|
||||
</Text>
|
||||
<Text variant="bodySmall" style={{ opacity: 0.7 }}>
|
||||
{user.email}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={{ alignSelf: "flex-start", marginLeft: "auto" }}>
|
||||
<IconButton
|
||||
icon="close"
|
||||
size={20}
|
||||
onPress={() => setVisible(false)}
|
||||
accessibilityLabel="Close dialog"
|
||||
style={{ margin: 0 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ height: 10 }} />
|
||||
<View style={{ flexDirection: "column", flexWrap: "wrap" }}>
|
||||
<Chip compact>{getRoleLabel(user.role)}</Chip>
|
||||
</View>
|
||||
</View>
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
title="Sign out"
|
||||
leadingIcon="logout"
|
||||
onPress={async () => {
|
||||
setVisible(false);
|
||||
await logout();
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { FormInput } from "@/components/auth/FormInput";
|
||||
import { MessageDisplay } from "@/components/auth/MessageDisplay";
|
||||
import { AuthButton } from "@/components/auth/AuthButton";
|
||||
|
||||
interface VerificationFormProps {
|
||||
code: string;
|
||||
setCode: (value: string) => void;
|
||||
errorMessage: string;
|
||||
isLoading: boolean;
|
||||
onVerifyPress: () => void;
|
||||
}
|
||||
|
||||
export const VerificationForm = ({
|
||||
code,
|
||||
setCode,
|
||||
errorMessage,
|
||||
isLoading,
|
||||
onVerifyPress,
|
||||
}: VerificationFormProps) => {
|
||||
return (
|
||||
<>
|
||||
<FormInput
|
||||
label="Verification Code"
|
||||
value={code}
|
||||
onChangeText={setCode}
|
||||
icon="key"
|
||||
keyboardType="number-pad"
|
||||
disabled={isLoading}
|
||||
style={{ marginBottom: 24 }}
|
||||
/>
|
||||
|
||||
{errorMessage ? (
|
||||
<MessageDisplay message={errorMessage} type="error" />
|
||||
) : null}
|
||||
|
||||
<AuthButton
|
||||
onPress={onVerifyPress}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Verify Email
|
||||
</AuthButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user