import * as React from "react"; import * as Actions from "~/common/actions"; import * as Events from "~/common/custom-events"; import * as Utilities from "~/common/utilities"; import * as Logging from "~/common/logging"; const AUTH_STATE_GRAPH = { initial: { SIGNIN: "signin", SIGNUP: "signup", SIGNUP_WITH_TWITTER: "twitter_signup", }, signin: { BACK: "initial", RESET_PASSWORD: "password_reset", }, signup: {}, twitter_signup: {}, password_reset: { BACK: "signin" }, }; const reducer = (state, { event, context }) => { const { scene, context: prevContext } = state; const nextScene = AUTH_STATE_GRAPH[scene][event]; return { context: { ...prevContext, ...context }, scene: nextScene !== undefined ? nextScene : scene, }; }; // NOTE(amine): handles switching between signup/signin states export const useAuthFlow = () => { const [state, send] = React.useReducer(reducer, { scene: "initial", context: {} }); const prevScene = React.useRef("initial"); const handlers = React.useMemo( () => ({ goToSigninScene: ({ emailOrUsername, message = "" }) => send({ event: "SIGNIN", context: { emailOrUsername, message } }), goToSignupScene: ({ email }) => send({ event: "SIGNUP", context: { email } }), goToTwitterSignupScene: ({ twitterEmail }) => send({ event: "SIGNUP_WITH_TWITTER", context: { twitterEmail } }), goToResetPassword: () => send({ event: "RESET_PASSWORD" }), goBack: () => send({ event: "BACK" }), clearMessages: () => send({ ...state, context: { ...state.context, message: "" } }), }), [] ); React.useEffect(() => { prevScene.current = state.scene; }, [state.scene]); return { ...handlers, ...state, prevScene: prevScene.current }; }; export const useSignin = ({ onAuthenticate }) => { const verificationToken = React.useRef(); const credentialsRef = React.useRef({ username: "", password: "" }); const signin = async ({ username, password }) => { const response = await Actions.getUserVersion({ username }); if (Events.hasError(response)) return; // NOTE(amine): handling client hash if the user is v2 let hashedPassword = await Utilities.encryptPasswordClient(password); credentialsRef.current = { username, password: hashedPassword }; //NOTE(amine): the onAuthenticate function will return early // if there is shouldMigrate in the response payload const passwordSentToServer = response?.data?.version === 1 ? password : hashedPassword; const authResponse = await onAuthenticate({ username: username.toLowerCase(), password: passwordSentToServer, }); if (authResponse.shouldMigrate) return authResponse; }; const createVerification = async (data) => { const response = await Actions.createLegacyVerification({ ...credentialsRef.current, ...data, }); if (Events.hasError(response)) { return; } verificationToken.current = response.token; return response; }; const migrateAccount = async ({ pin }) => { const response = await Actions.migrateUser({ token: verificationToken.current, pin }); if (Events.hasError(response)) return; const authResponse = await onAuthenticate(credentialsRef.current); if (Events.hasError(authResponse)) return; }; const resendVerification = async () => { const response = await Actions.resendVerification({ token: verificationToken.current, }); if (Events.hasError(response)) { return; } }; return { signin, createVerification, migrateAccount, resendVerification }; }; export const usePasswordReset = ({ onAuthenticate }) => { const verificationToken = React.useRef(); const emailRef = React.useRef(); const createVerification = async ({ email }) => { const response = await Actions.createPasswordResetVerification({ email }); if (Events.hasError(response)) return; emailRef.current = email; verificationToken.current = response.token; return response; }; const verifyEmail = async ({ pin }) => { const response = await Actions.verifyPasswordResetEmail({ pin, token: verificationToken.current, }); if (Events.hasError(response)) { return; } return response; }; const resendVerification = async () => { const response = await Actions.resendPasswordResetVerification({ token: verificationToken.current, }); if (Events.hasError(response)) { return; } }; const resetPassword = async ({ password }) => { const hashPassword = await Utilities.encryptPasswordClient(password); const response = await Actions.resetPassword({ password: hashPassword, token: verificationToken.current, }); if (Events.hasError(response)) { return; } await onAuthenticate({ username: emailRef.current, password: hashPassword }); }; return { createVerification, resendVerification, verifyEmail, resetPassword }; }; export const useSignup = ({ onAuthenticate }) => { const verificationToken = React.useRef(); const createVerification = async (data) => { const response = await Actions.createVerification(data); if (Events.hasError(response)) { return; } verificationToken.current = response.token; return response; }; const verifyEmail = async ({ pin }) => { const response = await Actions.verifyEmail({ pin, token: verificationToken.current }); if (Events.hasError(response)) { return; } return response; }; const createUser = async ({ username, password }) => { const hashPassword = await Utilities.encryptPasswordClient(password); const response = await Actions.createUser({ username, password: hashPassword, token: verificationToken.current, }); if (Events.hasError(response)) { return; } const authResponse = await onAuthenticate({ username, password: hashPassword }); if (Events.hasError(authResponse)) return; return authResponse; }; const resendVerification = async () => { const response = await Actions.resendVerification({ token: verificationToken.current, }); if (Events.hasError(response)) { return; } }; return { createVerification, verifyEmail, createUser, resendVerification }; }; export const useTwitter = ({ onAuthenticate, goToTwitterSignupScene }) => { const verificationToken = React.useRef(); const popupRef = React.useRef(); const [isLoggingIn, setIsLoggingIn] = React.useState(false); // NOTE(amine): will be used when signing up a user via twitter const twitterTokens = React.useRef({ authToken: "", authVerifier: "", }); const openPopup = () => { const width = 500; const height = 700; const left = (window.innerWidth - width) / 2; const top = (window.innerHeight - height) / 2; popupRef.current = window.open( "", "popup", ` width=${width},height=${height},left=${left},top=${top},toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no` ); }; const _getAuthTokenAndVerifier = () => new Promise((resolve, reject) => { const popup = popupRef.current; const interval = setInterval(() => { if (!popup || popup.closed || popup.closed === undefined) { clearInterval(interval); // TODO failure reject("getAuthTokenAndVerifier Error 1"); } const closeDialog = () => { clearInterval(interval); popup.close(); }; try { if ( !popup.location.hostname.includes("api.twitter.com") && !popup.location.hostname == "" ) { if (popup.location.search) { const query = new URLSearchParams(popup.location.search); const authToken = query.get("oauth_token"); const authVerifier = query.get("oauth_verifier"); closeDialog(); resolve({ authToken, authVerifier }); } else { if (popup) closeDialog(); // TODO: failure reject("getAuthTokenAndVerifier Error 2"); } } } catch (e) { Logging.error(e); } }, 500); }); const signin = async () => { setIsLoggingIn(true); openPopup(); const popup = popupRef.current; try { const { authToken } = await Actions.requestTwitterToken(); popup.location.href = `https://api.twitter.com/oauth/authenticate?oauth_token=${authToken}`; const responseVerifier = await _getAuthTokenAndVerifier(); if (Events.hasError(responseVerifier)) { return; } // NOTE(amine): update tokens twitterTokens.current = { authToken: responseVerifier.authToken, authVerifier: responseVerifier.authVerifier, }; const response = await Actions.authenticateViaTwitter({ authToken: responseVerifier.authToken, authVerifier: responseVerifier.authVerifier, }); if (Events.hasError(response)) { setIsLoggingIn(false); return; } if (response.token) { await onAuthenticate(response); setIsLoggingIn(false); return; } setIsLoggingIn(false); goToTwitterSignupScene({ twitterEmail: response.email }); } catch (e) { // TODO failure Logging.error("error", e); if (popup) popup.close(); setIsLoggingIn(false); } }; const signup = async ({ email = "", username = "" }) => { const { authToken } = twitterTokens.current; const response = await Actions.createUserViaTwitter({ email: email.toLowerCase(), username: username.toLowerCase(), authToken, }); if (Events.hasError(response)) return; if (response.token) { await onAuthenticate(response); return; } return response; }; const signupWithVerification = async ({ username = "", pin }) => { const response = await Actions.createUserViaTwitterWithVerification({ username: username.toLowerCase(), pin, token: verificationToken.current, }); if (Events.hasError(response)) return; if (response.token) { await onAuthenticate(response); return; } return response; }; const createVerification = async (data) => { const response = await Actions.createTwitterEmailVerification({ ...data, twitterToken: twitterTokens.current.authToken, }); if (Events.hasError(response)) { return; } verificationToken.current = response.token; return response; }; const resendVerification = async ({ email }) => { const response = await Actions.resendVerification({ email, token: verificationToken.current, }); if (Events.hasError(response)) { return; } }; return { isLoggingIn, signin, signup, signupWithVerification, createVerification, resendVerification, }; };