Skip to content

Commit

Permalink
Merge pull request #94 from lbare/update-otp-login
Browse files Browse the repository at this point in the history
Added OTP based login
  • Loading branch information
lbare authored Apr 3, 2024
2 parents d619091 + c7d7633 commit 8deeda6
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 152 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ package-lock.json
#dev_app.json is added by rahul to help in local deployment
dev_app.json

.env

# @generated expo-cli sync-b5df6a44d8735348b729920a7406b633cfb74d4c
# The following patterns were generated by expo-cli

Expand Down
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["nativewind/babel"],
plugins: ["nativewind/babel", "babel-plugin-inline-dotenv"],
};
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"@rneui/themed": "^4.0.0-rc.8",
"@types/jest": "^29.5.12",
"@types/react-test-renderer": "^18.0.7",
"babel-plugin-inline-dotenv": "^1.7.0",
"crypto-js": "^4.2.0",
"dotenv": "^16.4.5",
"expo": "~50.0.14",
"expo-asset": "^9.0.2",
"expo-build-properties": "^0.11.1",
Expand Down
286 changes: 156 additions & 130 deletions screens/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import {
Text,
TextInput,
Alert,
TouchableWithoutFeedback,
Keyboard,
ScrollView,
ActivityIndicator,
} from "react-native";
import FirebaseAuthManager from "../services/Firebase/firebase-auth";
import { useNavigation } from "@react-navigation/native";
import BackgroundImage from "../components/BackgroundImage";
import { CaretLeft, X } from "phosphor-react-native";

type LoginProps = {
modalVisible: boolean;
Expand All @@ -24,112 +24,148 @@ const Login: React.FC<LoginProps> = ({ modalVisible, setModalVisible }) => {
const [otpSent, setOtpSent] = useState(false);
const [infoMessage, setInfoMessage] = useState("");

const [errorType, setErrorType] = useState<
"email" | "password" | "both" | "firebase" | null
>(null);
const [errorType, setErrorType] = useState<"email" | "server" | null>(null);
const [errorMessage, setErrorMessage] = useState("");
const [emailFocused, setEmailFocused] = useState(false);
const [passwordFocused, setPasswordFocused] = useState(false);
const [loading, setLoading] = useState(false);
const [readyToFocus, setReadyToFocus] = useState(false);

const navigation = useNavigation();
const authManager = new FirebaseAuthManager();
const passwordRef = useRef(null);
const passwordRef = useRef<TextInput>(null);

useEffect(() => {
if (
email !== "" &&
!validateEmail() &&
password.length > 0 &&
!validatePassword()
) {
setError("both", "Email must be a valid UVic email");
} else if (email !== "" && !validateEmail()) {
if (email !== "" && !validateEmail()) {
setError("email", "Email must be a valid UVic email");
} else if (password.length > 0 && !validatePassword()) {
setError("password", "OTP must be 6 digit numbers");
} else {
setError(null, "");
}
}, [email, password]);
}, [email]);

// useEffect(() => {
// if (infoMessage) {
// const timer = setTimeout(() => {
// setInfoMessage("");
// }, 5000);

// return () => clearTimeout(timer);
// }
// }, [infoMessage]);

useEffect(() => {
if (errorType === "firebase") {
if (otpSent) {
setTimeout(() => {
setErrorMessage("");
}, 2000);
setReadyToFocus(true);
}, 300);
}
}, [errorType]);
}, [otpSent]);

const handleSignIn = async () => {
try {
const response = authManager.initiatePasswordlessSignIn(email);
response
.then((res) => {
return res.json();
})
.then((json) => {
setOtpSent(true);
setInfoMessage("6-digit code has been sent to your email.");
})
.catch((error) => {
console.error(error);
});
} catch (error) {
console.error(error);
setError(
"firebase",
"Failed to sign in. Please check your email and password."
);
useEffect(() => {
if (readyToFocus && passwordRef.current) {
console.log("focusing");

passwordRef.current.focus();
setReadyToFocus(false);
}
}, [readyToFocus]);

const handleSignIn = async () => {
setLoading(true);

const response = authManager.initiatePasswordlessSignIn(email);
response
.then(() => {
setOtpSent(true);
setInfoMessage("6-digit code has been sent to " + email);
setLoading(false);
})
.catch((error) => {
setError("server", error);
setLoading(false);
});
};

const handleVerifyOTP = async () => {
setModalVisible(false);
navigation.goBack();
setLoading(true);
authManager
.handleSignInWithOTP(email, password)
.then((success) => {
if (success) {
Alert.alert("Success", "Successfully logged in as " + email);
setModalVisible(false);
navigation.goBack();
}
setLoading(false);
})
.catch((error) => {
setError("server", error);
setLoading(false);
});
};

const setError = (
field: "email" | "password" | "both" | "firebase" | null,
message: string
) => {
const setError = (field: "email" | "server" | null, message: string) => {
setErrorType(field);
setErrorMessage(message);
};

const validateEmail = () => email.endsWith("@uvic.ca");
const validatePassword = () => {
return password.length == 6 &&
!isNaN(Number(password));
}

const backToEmail = () => {
setOtpSent(false);
setPassword("");
setInfoMessage("");
};

return (
<ScrollView
contentContainerStyle={{
justifyContent: "flex-start",
justifyContent: "center",
flex: 1,
width: "100%",
height: "100%",
}}
scrollEnabled={false}
keyboardShouldPersistTaps="handled"
>
<TouchableWithoutFeedback onPress={() => Keyboard.dismiss()}>
<>
<BackgroundImage source={require("../assets/splash-login.png")} />
<View className="flex justify-center items-center mt-96 px-10">
{otpSent ? (
<TouchableOpacity
className="absolute top-8 left-0 z-10 h-24 w-24 justify-center items-center"
onPress={backToEmail}
>
<View className="justify-center items-center rounded-full h-8 w-8 bg-blue">
<CaretLeft size={24} color="#ededed" weight="bold" />
</View>
</TouchableOpacity>
) : (
<TouchableOpacity
className="absolute top-8 left-0 z-10 h-24 w-24 justify-center items-center"
onPress={() => {
navigation.goBack();
}}
>
<View className="justify-center items-center rounded-full h-8 w-8 bg-blue">
<X size={24} color="#ededed" weight="bold" />
</View>
</TouchableOpacity>
)}
<>
<BackgroundImage source={require("../assets/splash-login.png")} />
<View className="flex h-80 pb-20 justify-end items-center px-10">
{!otpSent ? (
<TextInput
placeholder="UVic Email"
returnKeyType="next"
returnKeyType="send"
value={email}
onChangeText={(text) => {
setEmail(text);
setEmail(text.toLowerCase());
}}
keyboardType="email-address"
autoCapitalize="none"
className={`w-full h-12 border-2 bg-white rounded-lg px-4 mb-3 ${
errorType === "email" ||
errorType === "both" ||
errorType === "firebase"
className={`w-full h-12 border-2 bg-white rounded-lg px-4 ${
errorType === "email"
? "border-orange"
: passwordFocused || password !== ""
: email !== "" || emailFocused
? "border-blue"
: "border-neutral-400"
}`}
Expand All @@ -141,81 +177,71 @@ const Login: React.FC<LoginProps> = ({ modalVisible, setModalVisible }) => {
}}
editable={!otpSent}
onBlur={() => {
if (validateEmail() && passwordRef.current) {
(passwordRef.current as any).focus();
setPasswordFocused(true);
if (validateEmail()) {
handleSignIn();
}
}}
/>
) : (
<TextInput
ref={passwordRef}
placeholder="One Time Password"
value={password}
returnKeyType="done"
editable={!loading}
onChangeText={(text) => {
setPassword(text);
}}
className={`w-full h-12 border-2 bg-white rounded-lg px-4 ${
passwordFocused && password !== ""
? "border-blue"
: "border-neutral-400"
}`}
style={{
fontSize: 16,
color: "#154058",
fontWeight: "bold",
}}
keyboardType="phone-pad"
onBlur={() => {
setPasswordFocused(false);
}}
onFocus={() => {
setEmailFocused(true);
if (errorType === "firebase") setError(null, "");
setPasswordFocused(true);
}}
/>
{
infoMessage !== "" && (
<View
className="flex h-4 justify-center items-center px-2 rounded-lg mb-2 bg-white"
>
<Text className="text-center text-xs font-bold text-blue">
{infoMessage}
</Text>
</View>
)
}
{otpSent && (
<TextInput
ref={passwordRef}
placeholder="Password"
value={password}
returnKeyType="done"
onChangeText={(text) => {
setPassword(text);
}}
className={`w-full h-12 border-2 bg-white rounded-lg px-4 ${
errorType === "password" ||
errorType === "both" ||
errorType === "firebase"
? "border-orange"
: passwordFocused || password !== ""
? "border-blue"
: "border-neutral-400"
}`}
style={{
fontSize: 16,
color: "#154058",
fontWeight: "bold",
}}
keyboardType = "phone-pad"
onBlur={() => {
setPasswordFocused(false);
}}
onFocus={() => {
setPasswordFocused(true);
if (errorType === "firebase") setError(null, "");
}}
/>
)}

<View
className={`flex h-10 justify-center items-center px-2 rounded-b-lg mb-4 bg-white ${
!errorMessage && "opacity-0"
)}
<View
className={`flex w-full h-10 justify-center items-center px-2 rounded-b-lg bg-white ${
!errorMessage && !infoMessage && "opacity-0"
}`}
>
<Text
className={`text-center font-bold ${
errorMessage ? "text-orange" : "text-blue"
}`}
>
<Text className="text-center font-bold text-orange">
{errorMessage}
</Text>
</View>

<TouchableOpacity
onPress={() => otpSent ? handleVerifyOTP():handleSignIn()}
disabled={errorType !== null}
className="w-full h-12 rounded-full justify-center items-center mb-4 bg-orange"
>
<Text className="text-white font-bold text-base"> {otpSent ? "Verify OTP":"Sign In"}</Text>
</TouchableOpacity>
{errorMessage || infoMessage}
</Text>
</View>
</>
</TouchableWithoutFeedback>

<TouchableOpacity
onPress={() => (otpSent ? handleVerifyOTP() : handleSignIn())}
disabled={
loading || errorType == "email" || (otpSent && password === "")
}
className="w-full h-12 rounded-full justify-center items-center mb-4 bg-orange"
>
{!loading ? (
<Text className="text-white font-bold text-base">
{otpSent ? "Verify OTP" : "Send Code"}
</Text>
) : (
<ActivityIndicator size="small" color="#fff" />
)}
</TouchableOpacity>
</View>
</>
</ScrollView>
);
};
Expand Down
Loading

0 comments on commit 8deeda6

Please sign in to comment.