Skip to content

Recovery code login flow #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions src/components/Login.js
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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) => {
Expand All @@ -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);
Expand All @@ -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);
}
Expand All @@ -95,8 +113,12 @@ export default function Login({ setUser }) {
<EmailOTPModal login={otpLogin} handleCancel={handleCancel} />
) : showMFAOTPModal ? (
<MFAOTPModal handle={otpLogin} handleCancel={handleCancel} />
) : showRecoveryCodeModal ? (
<RecoveryCodeModal handle={otpLogin} handleCancel={handleCancel} />
) : (
<EmailForm handleEmailLoginCustom={handleEmailLoginCustom} />
<div>
<EmailForm handleEmailLoginCustom={handleEmailLoginCustom} />
</div>
)}
</div>
);
Expand Down
44 changes: 39 additions & 5 deletions src/components/MFA/MFAOTPModal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from "react";
import { LoginWithEmailOTPEventEmit } from "magic-sdk";

export default function MFAOTPModal({ handle, handleCancel }) {
const [passcode, setPasscode] = useState("");
Expand Down Expand Up @@ -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(
Expand All @@ -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 (
<div className="modal">
<h1>enter the code from your authenticator app</h1>
<h1>Enter the code from your authenticator app</h1>

{message && (
<div className="message-wrapper">
Expand Down Expand Up @@ -71,7 +92,7 @@ export default function MFAOTPModal({ handle, handleCancel }) {
}}
disabled={disabled}
>
cancel
Cancel
</button>
<button
className="ok-button mfa-otp-submit"
Expand All @@ -81,6 +102,19 @@ export default function MFAOTPModal({ handle, handleCancel }) {
Submit
</button>
</div>

<div
className="lost-device-container"
style={{ marginTop: "20px", textAlign: "center" }}
>
<button
className="text-button lost-device-button"
onClick={handleLostDevice}
disabled={disabled}
>
Lost your device? Use recovery code
</button>
</div>
</div>
);
}
112 changes: 112 additions & 0 deletions src/components/RecoveryCodeModal.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="modal">
<h1>Enter your recovery code</h1>
<p>Please enter the recovery code you received during MFA setup.</p>

{message && (
<div className="message-wrapper">
<code id="recovery-message">{message}</code>
</div>
)}

<form className="otp-form" onSubmit={handleSubmit}>
<input
type="text"
name="recoveryCode"
id="recoveryCode"
placeholder="Enter recovery code"
value={recoveryCode}
onChange={(e) => setRecoveryCode(e.target.value.replace(/\s/g, ""))}
disabled={disabled}
autoFocus
/>
</form>

<div className="modal-footer">
<button
className="cancel-button"
onClick={() => {
handle.emit(LoginWithEmailOTPEventEmit.Cancel);
handleCancel();
}}
disabled={disabled}
>
Cancel
</button>
<button
className="ok-button recovery-submit"
disabled={disabled}
onClick={handleSubmit}
>
Submit
</button>
</div>
</div>
);
}