Just-in-time MFA prompt
Ask user to enable MFA at a teachable moment
If you require MFA at signup, you'll lose conversions. If you never require, adoption is < 10%. Middle path: ask at the right moments.
Teachable moments
The user is paying attention and feels the value when:
- They're about to do something important (first payment, first invite to a teammate).
- They've recently been impacted by a related event (signed in from a new device).
- They've completed a milestone (used the product for 7 days).
- They've expressed care about security (toggled a "make this secure" setting).
These are NOT teachable:
- The moment of signup (they want to see the app).
- Right after creating an account (overwhelmed).
- When they're about to leave.
Pattern
Soft prompt at teachable moment, dismissible:
{!user.has_mfa && shouldPromptMfa(user) && (
<Modal>
<h2>Add 2-factor authentication?</h2>
<p>Your account holds sensitive data. 2FA protects against compromised passwords.</p>
<p>Setup takes 2 minutes.</p>
<Button onClick={enrollMfa}>Set up 2FA</Button>
<Button variant="ghost" onClick={dismiss}>Maybe later</Button>
</Modal>
)}Trigger logic
function shouldPromptMfa(user) {
if (user.has_mfa) return false;
if (user.dismissed_mfa_recently) return false;
const triggers = [
user.actions_count > 10, // engaged user
user.created_at < 7daysAgo, // not brand new
user.last_payment_at, // monetized
user.invited_others_count > 0, // collaborator
user.security_event_recently, // had a scare
];
return triggers.filter(Boolean).length >= 2;
}Multiple signals = clearer benefit.
Dismissal tracking
CREATE TABLE mfa_prompts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
identity_id UUID NOT NULL,
prompted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
response TEXT NOT NULL, -- "enrolled" | "dismissed" | "later"
reason TEXT
);After 3 dismissals, escalate? Or give up?
Track conversion rate. If "dismissed" rate is high, the prompt isn't working, change wording or moment.
"Later" with reminder
<Button onClick={dismissAndRemind}>Remind me in a week</Button>async function dismissAndRemind() {
await db.insert(reminders).values({
identity_id: user.id,
type: "mfa",
remind_at: addDays(new Date(), 7),
});
}Cron picks up; emails reminder.
Force at threshold
For some milestones, prompt becomes mandatory:
function mustEnrollMfa(user, action) {
if (action === "transfer_money_over_1000") return true;
if (action === "delete_account") return true;
if (action === "view_compliance_data") return true;
return false;
}if (mustEnrollMfa(user, action) && !user.has_mfa) {
return <ForceMfaEnrollment />;
}User can't proceed without enrolling.
Frame it positively
Bad: "You must enable 2FA." Good: "Add 2FA to protect your account. Takes 2 minutes."
Bad: "Required for compliance." Good: "Your data is sensitive. Let's add an extra layer."
People comply more for self-interest than compliance.
Choose method UX
When user clicks "enroll," offer choices:
<RadioGroup>
<Radio value="webauthn">
Use this device's biometric (Face ID / Touch ID)
<p>Most convenient.</p>
</Radio>
<Radio value="totp">
Authenticator app (Google Authenticator, Authy, 1Password)
<p>Works without internet.</p>
</Radio>
<Radio value="sms">
Text message
<p>Easy but less secure.</p>
</Radio>
</RadioGroup>Default to WebAuthn / passkey if device supports.
Don't make MFA scary
"To protect your account, scan this QR code with your authenticator app:"
[QR]
"After scanning, enter the 6-digit code shown in your app."
[Input]Not:
"Generate a TOTP shared secret keyed with HMAC-SHA1 and provision a..." Plain language.
After enrollment
Celebrate:
<Modal>
<h2>✓ 2-factor enabled</h2>
<p>Your account is now significantly more secure.</p>
<h3>Save your recovery codes</h3>
<p>If you lose your authenticator, use one of these codes:</p>
<CodeList codes={recoveryCodes} />
<Checkbox label="I've saved my codes" />
<Button>Done</Button>
</Modal>Reward, not chore.
Analytics
Track:
- Prompts shown.
- Dismissed (by reason if asked).
- Enrolled.
- After enrollment, MFA challenges per session.
If enrollment is high but challenge-success is low: the UX is broken, TOTP codes not working, etc.
Don't promote SMS
SMS MFA has known weaknesses (SIM swap). Promote stronger:
<Radio value="sms" disabled={highSecurityAccount}>
SMS (not recommended for high-value accounts)
</Radio>For admin / financial: hide SMS.