diff --git a/src/components/Login.js b/src/components/Login.js index 81be1d2..0fe3441 100644 --- a/src/components/Login.js +++ b/src/components/Login.js @@ -1,13 +1,20 @@ import React, { useState } from "react"; import { magic } from "../lib/magic.js"; +import { + DisableMFAEventOnReceived, + LoginWithEmailOTPEventOnReceived, + LoginWithEmailOTPEventEmit, +} from "magic-sdk"; import EmailOTPModal from "./EmailOTPModal.js"; import EmailForm from "./EmailForm.js"; import DeviceRegistration from "./DeviceRegistration.js"; import MFAOTPModal from "./MFA/MFAOTPModal.js"; +import RecoveryCodeModal from "./RecoveryCodeModal.js"; export default function Login({ setUser }) { const [showEmailOTPModal, setShowEmailOTPModal] = useState(false); const [showMFAOTPModal, setShowMFAOTPModal] = useState(false); + const [showRecoveryCodeModal, setShowRecoveryCodeModal] = useState(false); const [showDeviceRegistrationModal, setShowDeviceRegistrationModal] = useState(false); const [otpLogin, setOtpLogin] = useState(); @@ -25,22 +32,18 @@ export default function Login({ setUser }) { otpLogin .on("device-needs-approval", () => { // is called when device is not recognized and requires approval - setShowDeviceRegistrationModal(true); }) .on("device-approved", () => { // is called when the device has been approved - setShowDeviceRegistrationModal(false); }) .on("email-otp-sent", () => { // The email has been sent to the user - setShowEmailOTPModal(true); }) .on("done", (result) => { handleGetMetadata(); - console.log(`DID Token: %c${result}`, "color: orange"); }) .catch((err) => { @@ -53,12 +56,22 @@ export default function Login({ setUser }) { setShowEmailOTPModal(false); setShowMFAOTPModal(false); setShowDeviceRegistrationModal(false); + setShowRecoveryCodeModal(false); }) .on("mfa-sent-handle", (mfaHandle) => { // Display the MFA OTP modal - + console.log("MFA sent handle received, showing MFA modal"); setShowEmailOTPModal(false); setShowMFAOTPModal(true); + }) + .on("recovery-code-sent-handle", () => { + // This is critical for the recovery flow + console.log( + "RecoveryCodeSentHandle received, showing recovery code modal" + ); + setShowEmailOTPModal(false); + setShowMFAOTPModal(false); + setShowRecoveryCodeModal(true); }); } catch (err) { console.error(err); @@ -67,17 +80,22 @@ export default function Login({ setUser }) { const handleGetMetadata = async () => { const metadata = await magic.user.getInfo(); - setUser(metadata); - console.table(metadata); }; const handleCancel = () => { try { - otpLogin.emit("cancel"); + if (otpLogin) { + otpLogin.emit("cancel"); + console.log("%cUser canceled login.", "color: orange"); + } - console.log("%cUser canceled login.", "color: orange"); + // Reset all UI states + setShowEmailOTPModal(false); + setShowMFAOTPModal(false); + setShowRecoveryCodeModal(false); + setShowDeviceRegistrationModal(false); } catch (err) { console.log("Error canceling login:", err); } @@ -95,8 +113,12 @@ export default function Login({ setUser }) { ) : showMFAOTPModal ? ( + ) : showRecoveryCodeModal ? ( + ) : ( - +
+ +
)} ); diff --git a/src/components/MFA/MFAOTPModal.js b/src/components/MFA/MFAOTPModal.js index fa0aaf1..79e96ab 100644 --- a/src/components/MFA/MFAOTPModal.js +++ b/src/components/MFA/MFAOTPModal.js @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { LoginWithEmailOTPEventEmit } from "magic-sdk"; export default function MFAOTPModal({ handle, handleCancel }) { const [passcode, setPasscode] = useState(""); @@ -27,9 +28,8 @@ export default function MFAOTPModal({ handle, handleCancel }) { setDisabled(false); if (!retries) { - setMessage("No more retries. Please try again later."); - - handleCancel(); + setMessage("No more retries. Please try recovery flow."); + // Instead of canceling, offer recovery option } else { // Prompt the user again for the MFA OTP setMessage( @@ -41,9 +41,30 @@ export default function MFAOTPModal({ handle, handleCancel }) { }); }; + const handleLostDevice = () => { + // This is the key function that initiates the recovery flow + console.log("Initiating recovery flow due to lost device"); + try { + // Emit the LostDevice event to trigger the recovery flow + console.log("Emitting LostDevice event"); + handle.emit(LoginWithEmailOTPEventEmit.LostDevice); + + // RecoveryCodeSentHandle event will be handled by the parent component + // which will show the RecoveryCodeModal + console.log("Waiting for RecoveryCodeSentHandle event"); + + // Disable the UI while we wait + setDisabled(true); + setMessage("Initiating recovery flow... Please wait."); + } catch (error) { + console.error("Error initiating recovery flow:", error); + setDisabled(false); + } + }; + return (
-

enter the code from your authenticator app

+

Enter the code from your authenticator app

{message && (
@@ -71,7 +92,7 @@ export default function MFAOTPModal({ handle, handleCancel }) { }} disabled={disabled} > - cancel + Cancel
+ +
+ +
); } diff --git a/src/components/RecoveryCodeModal.js b/src/components/RecoveryCodeModal.js new file mode 100644 index 0000000..11d8f1d --- /dev/null +++ b/src/components/RecoveryCodeModal.js @@ -0,0 +1,112 @@ +import React, { useState, useEffect } from "react"; +import { + LoginWithEmailOTPEventEmit, + LoginWithEmailOTPEventOnReceived, +} from "magic-sdk"; + +export default function RecoveryCodeModal({ handle, handleCancel }) { + const [recoveryCode, setRecoveryCode] = useState(""); + const [message, setMessage] = useState(""); + const [disabled, setDisabled] = useState(false); + + // Add listener for InvalidRecoveryCode event + useEffect(() => { + if (!handle) return; + + const invalidCodeListener = () => { + console.log("Invalid recovery code received"); + setDisabled(false); + setMessage("Invalid recovery code. Please try again."); + }; + + // Register listener for InvalidRecoveryCode event + handle.on( + LoginWithEmailOTPEventOnReceived.InvalidRecoveryCode, + invalidCodeListener + ); + + // Clean up the listener when component unmounts + return () => { + handle.off( + LoginWithEmailOTPEventOnReceived.InvalidRecoveryCode, + invalidCodeListener + ); + }; + }, [handle]); + + // This component is only shown after RecoveryCodeSentHandle event has been received, + // so we can directly submit the recovery code when the user clicks Submit. + + const handleSubmit = async (e) => { + if (e) e.preventDefault(); + + if (!recoveryCode.trim()) { + setMessage("Please enter your recovery code"); + return; + } + + setDisabled(true); + setMessage("Verifying recovery code..."); + + try { + // The lost device event has already been emitted and RecoveryCodeSentHandle + // has already been received (that's why this modal is showing), + // so we can directly submit the recovery code. + console.log("Submitting recovery code for verification"); + handle.emit(LoginWithEmailOTPEventEmit.VerifyRecoveryCode, recoveryCode); + + // Reset the input field + setRecoveryCode(""); + } catch (error) { + console.error("Error submitting recovery code:", error); + setDisabled(false); + setMessage("Error submitting recovery code. Please try again."); + } + }; + + return ( +
+

Enter your recovery code

+

Please enter the recovery code you received during MFA setup.

+ + {message && ( +
+ {message} +
+ )} + +
+ setRecoveryCode(e.target.value.replace(/\s/g, ""))} + disabled={disabled} + autoFocus + /> +
+ +
+ + +
+
+ ); +}