Skip to content

Commit 41ef1b7

Browse files
authored
Merge pull request #1 from T-adero1/recovery-code
Implement proper recovery code flow through MFA path
2 parents 26ce0f2 + c5376e1 commit 41ef1b7

File tree

3 files changed

+155
-15
lines changed

3 files changed

+155
-15
lines changed

src/components/Login.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import React, { useState } from "react";
22
import { magic } from "../lib/magic.js";
3+
import {
4+
DisableMFAEventOnReceived,
5+
LoginWithEmailOTPEventOnReceived,
6+
LoginWithEmailOTPEventEmit,
7+
} from "magic-sdk";
38
import EmailOTPModal from "./EmailOTPModal.js";
49
import EmailForm from "./EmailForm.js";
510
import DeviceRegistration from "./DeviceRegistration.js";
611
import MFAOTPModal from "./MFA/MFAOTPModal.js";
12+
import RecoveryCodeModal from "./RecoveryCodeModal.js";
713

814
export default function Login({ setUser }) {
915
const [showEmailOTPModal, setShowEmailOTPModal] = useState(false);
1016
const [showMFAOTPModal, setShowMFAOTPModal] = useState(false);
17+
const [showRecoveryCodeModal, setShowRecoveryCodeModal] = useState(false);
1118
const [showDeviceRegistrationModal, setShowDeviceRegistrationModal] =
1219
useState(false);
1320
const [otpLogin, setOtpLogin] = useState();
@@ -25,22 +32,18 @@ export default function Login({ setUser }) {
2532
otpLogin
2633
.on("device-needs-approval", () => {
2734
// is called when device is not recognized and requires approval
28-
2935
setShowDeviceRegistrationModal(true);
3036
})
3137
.on("device-approved", () => {
3238
// is called when the device has been approved
33-
3439
setShowDeviceRegistrationModal(false);
3540
})
3641
.on("email-otp-sent", () => {
3742
// The email has been sent to the user
38-
3943
setShowEmailOTPModal(true);
4044
})
4145
.on("done", (result) => {
4246
handleGetMetadata();
43-
4447
console.log(`DID Token: %c${result}`, "color: orange");
4548
})
4649
.catch((err) => {
@@ -53,12 +56,22 @@ export default function Login({ setUser }) {
5356
setShowEmailOTPModal(false);
5457
setShowMFAOTPModal(false);
5558
setShowDeviceRegistrationModal(false);
59+
setShowRecoveryCodeModal(false);
5660
})
5761
.on("mfa-sent-handle", (mfaHandle) => {
5862
// Display the MFA OTP modal
59-
63+
console.log("MFA sent handle received, showing MFA modal");
6064
setShowEmailOTPModal(false);
6165
setShowMFAOTPModal(true);
66+
})
67+
.on("recovery-code-sent-handle", () => {
68+
// This is critical for the recovery flow
69+
console.log(
70+
"RecoveryCodeSentHandle received, showing recovery code modal"
71+
);
72+
setShowEmailOTPModal(false);
73+
setShowMFAOTPModal(false);
74+
setShowRecoveryCodeModal(true);
6275
});
6376
} catch (err) {
6477
console.error(err);
@@ -67,17 +80,22 @@ export default function Login({ setUser }) {
6780

6881
const handleGetMetadata = async () => {
6982
const metadata = await magic.user.getInfo();
70-
7183
setUser(metadata);
72-
7384
console.table(metadata);
7485
};
7586

7687
const handleCancel = () => {
7788
try {
78-
otpLogin.emit("cancel");
89+
if (otpLogin) {
90+
otpLogin.emit("cancel");
91+
console.log("%cUser canceled login.", "color: orange");
92+
}
7993

80-
console.log("%cUser canceled login.", "color: orange");
94+
// Reset all UI states
95+
setShowEmailOTPModal(false);
96+
setShowMFAOTPModal(false);
97+
setShowRecoveryCodeModal(false);
98+
setShowDeviceRegistrationModal(false);
8199
} catch (err) {
82100
console.log("Error canceling login:", err);
83101
}
@@ -95,8 +113,12 @@ export default function Login({ setUser }) {
95113
<EmailOTPModal login={otpLogin} handleCancel={handleCancel} />
96114
) : showMFAOTPModal ? (
97115
<MFAOTPModal handle={otpLogin} handleCancel={handleCancel} />
116+
) : showRecoveryCodeModal ? (
117+
<RecoveryCodeModal handle={otpLogin} handleCancel={handleCancel} />
98118
) : (
99-
<EmailForm handleEmailLoginCustom={handleEmailLoginCustom} />
119+
<div>
120+
<EmailForm handleEmailLoginCustom={handleEmailLoginCustom} />
121+
</div>
100122
)}
101123
</div>
102124
);

src/components/MFA/MFAOTPModal.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from "react";
2+
import { LoginWithEmailOTPEventEmit } from "magic-sdk";
23

34
export default function MFAOTPModal({ handle, handleCancel }) {
45
const [passcode, setPasscode] = useState("");
@@ -27,9 +28,8 @@ export default function MFAOTPModal({ handle, handleCancel }) {
2728
setDisabled(false);
2829

2930
if (!retries) {
30-
setMessage("No more retries. Please try again later.");
31-
32-
handleCancel();
31+
setMessage("No more retries. Please try recovery flow.");
32+
// Instead of canceling, offer recovery option
3333
} else {
3434
// Prompt the user again for the MFA OTP
3535
setMessage(
@@ -41,9 +41,30 @@ export default function MFAOTPModal({ handle, handleCancel }) {
4141
});
4242
};
4343

44+
const handleLostDevice = () => {
45+
// This is the key function that initiates the recovery flow
46+
console.log("Initiating recovery flow due to lost device");
47+
try {
48+
// Emit the LostDevice event to trigger the recovery flow
49+
console.log("Emitting LostDevice event");
50+
handle.emit(LoginWithEmailOTPEventEmit.LostDevice);
51+
52+
// RecoveryCodeSentHandle event will be handled by the parent component
53+
// which will show the RecoveryCodeModal
54+
console.log("Waiting for RecoveryCodeSentHandle event");
55+
56+
// Disable the UI while we wait
57+
setDisabled(true);
58+
setMessage("Initiating recovery flow... Please wait.");
59+
} catch (error) {
60+
console.error("Error initiating recovery flow:", error);
61+
setDisabled(false);
62+
}
63+
};
64+
4465
return (
4566
<div className="modal">
46-
<h1>enter the code from your authenticator app</h1>
67+
<h1>Enter the code from your authenticator app</h1>
4768

4869
{message && (
4970
<div className="message-wrapper">
@@ -71,7 +92,7 @@ export default function MFAOTPModal({ handle, handleCancel }) {
7192
}}
7293
disabled={disabled}
7394
>
74-
cancel
95+
Cancel
7596
</button>
7697
<button
7798
className="ok-button mfa-otp-submit"
@@ -81,6 +102,19 @@ export default function MFAOTPModal({ handle, handleCancel }) {
81102
Submit
82103
</button>
83104
</div>
105+
106+
<div
107+
className="lost-device-container"
108+
style={{ marginTop: "20px", textAlign: "center" }}
109+
>
110+
<button
111+
className="text-button lost-device-button"
112+
onClick={handleLostDevice}
113+
disabled={disabled}
114+
>
115+
Lost your device? Use recovery code
116+
</button>
117+
</div>
84118
</div>
85119
);
86120
}

src/components/RecoveryCodeModal.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, { useState } from "react";
2+
import { LoginWithEmailOTPEventEmit } from "magic-sdk";
3+
4+
export default function RecoveryCodeModal({ handle, handleCancel }) {
5+
const [recoveryCode, setRecoveryCode] = useState("");
6+
const [message, setMessage] = useState("");
7+
const [disabled, setDisabled] = useState(false);
8+
9+
// This component is only shown after RecoveryCodeSentHandle event has been received,
10+
// so we can directly submit the recovery code when the user clicks Submit.
11+
12+
const handleSubmit = async (e) => {
13+
if (e) e.preventDefault();
14+
15+
if (!recoveryCode.trim()) {
16+
setMessage("Please enter your recovery code");
17+
return;
18+
}
19+
20+
setDisabled(true);
21+
setMessage("Verifying recovery code...");
22+
23+
try {
24+
// The lost device event has already been emitted and RecoveryCodeSentHandle
25+
// has already been received (that's why this modal is showing),
26+
// so we can directly submit the recovery code.
27+
console.log("Submitting recovery code for verification");
28+
handle.emit(LoginWithEmailOTPEventEmit.VerifyRecoveryCode, recoveryCode);
29+
30+
// Reset the input field
31+
setRecoveryCode("");
32+
} catch (error) {
33+
console.error("Error submitting recovery code:", error);
34+
setDisabled(false);
35+
setMessage("Error submitting recovery code. Please try again.");
36+
}
37+
};
38+
39+
return (
40+
<div className="modal">
41+
<h1>Enter your recovery code</h1>
42+
<p>Please enter the recovery code you received during MFA setup.</p>
43+
44+
{message && (
45+
<div className="message-wrapper">
46+
<code id="recovery-message">{message}</code>
47+
</div>
48+
)}
49+
50+
<form className="otp-form" onSubmit={handleSubmit}>
51+
<input
52+
type="text"
53+
name="recoveryCode"
54+
id="recoveryCode"
55+
placeholder="Enter recovery code"
56+
value={recoveryCode}
57+
onChange={(e) => setRecoveryCode(e.target.value.replace(/\s/g, ""))}
58+
disabled={disabled}
59+
autoFocus
60+
/>
61+
</form>
62+
63+
<div className="modal-footer">
64+
<button
65+
className="cancel-button"
66+
onClick={() => {
67+
handle.emit(LoginWithEmailOTPEventEmit.Cancel);
68+
handleCancel();
69+
}}
70+
disabled={disabled}
71+
>
72+
Cancel
73+
</button>
74+
<button
75+
className="ok-button recovery-submit"
76+
disabled={disabled}
77+
onClick={handleSubmit}
78+
>
79+
Submit
80+
</button>
81+
</div>
82+
</div>
83+
);
84+
}

0 commit comments

Comments
 (0)