Auth Mfa
blockauthTwo-step verification surface. 6-digit OTP pin input auto-submits on entry, shows verifying state and an inline error on mismatch, has a 30-second resend cooldown, and swaps to a Verified card with a Continue button on success. Emits `verify` (code), `resend`, and `continue`. `demoCode` prop controls the value the built-in mock validator accepts.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/auth-mfa.json$npx shadcn@latest add https://uipkge.dev/r/lit/auth-mfa.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/auth-mfa.json$bunx shadcn@latest add https://uipkge.dev/r/lit/auth-mfa.jsonManifest:
https://uipkge.dev/r/lit/auth-mfa.jsonInstalls to:src/uip/blocks/auth-mfa/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
heading | string | Two-step verification | optional |
description | string | Enter the 6-digit code from your authenticator app. | optional |
continue-href | string | / | optional |
recovery-href | string | # | optional |
demo-code | string | 123456 | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-auth-mfa': UipAuthMfa
}
}Files installed (1)
src/uip/blocks/auth-mfa/auth-mfa.ts7 kB
import { LitElement, css, html, nothing } from 'lit' import { RotateCw, ShieldCheck } from 'lucide' import { cn } from '../../lib/utils' import { tailwind } from '../../lib/styles' import { ThemeController } from '../../lib/theme' import { icon } from '../../lib/icon' // Side-effect imports: register the composed elements for standalone use // (consumers who install only this block still get working primitives). import '../../components/button/button' import '../../components/card/card' import '../../components/pin-input/pin-input' import type { UipPinInput } from '../../components/pin-input/pin-input' /** * <uip-auth-mfa> — the registry AuthMfa block as a web component. * * Class strings are React's AuthMfa verbatim. React state (useState / * useEffect) becomes @state fields; the code-length effect becomes an * `@input` check that auto-submits at 6 digits. `onVerify` (code) / * `onResend` / `onContinue` become `verify` / `resend` / `continue` * CustomEvents (bubbles, composed). `demoCode` is the value the built-in * mock validator accepts. */ export class UipAuthMfa extends LitElement { static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true } // :host display can't be a utility class (the host has no template of its own). static styles = [tailwind, css`:host { display: block; }`] static properties = { heading: {}, description: {}, continueHref: { attribute: 'continue-href' }, recoveryHref: { attribute: 'recovery-href' }, demoCode: { attribute: 'demo-code' }, code: { state: true }, verifying: { state: true }, verified: { state: true }, error: { state: true }, resendIn: { state: true }, } /** React's `title` (`title` is taken by HTMLElement). */ heading = 'Two-step verification' description = 'Enter the 6-digit code from your authenticator app.' continueHref = '/' recoveryHref = '#' demoCode = '123456' private code = '' private verifying = false private verified = false private error = '' private resendIn = 0 private verifyTimer?: number private resendTimer?: number constructor() { super() new ThemeController(this) } connectedCallback() { super.connectedCallback() this.setAttribute('data-uipkge', '') this.setAttribute('data-slot', 'auth-mfa') } disconnectedCallback() { super.disconnectedCallback() if (this.verifyTimer) window.clearTimeout(this.verifyTimer) if (this.resendTimer) window.clearInterval(this.resendTimer) } private onCodeInput(e: Event) { const next = (e.target as UipPinInput).value this.code = next if (next.length !== 6 || this.verifying) return this.error = '' this.verifying = true this.dispatchEvent(new CustomEvent('verify', { detail: next, bubbles: true, composed: true })) this.verifyTimer = window.setTimeout(() => { this.verifying = false if (this.code === this.demoCode) { this.verified = true } else { this.error = `Invalid code. Try ${this.demoCode} for the demo.` this.code = '' } }, 700) } private startResendCooldown() { this.resendIn = 30 this.dispatchEvent(new CustomEvent('resend', { bubbles: true, composed: true })) if (this.resendTimer) window.clearInterval(this.resendTimer) this.resendTimer = window.setInterval(() => { this.resendIn -= 1 if (this.resendIn <= 0) { this.resendIn = 0 if (this.resendTimer) window.clearInterval(this.resendTimer) } }, 1000) } render() { return html` <div data-slot="auth-mfa" class=${cn('bg-background flex min-h-svh items-center justify-center p-6')}> <uip-card class="w-full max-w-sm"> ${!this.verified ? html` <uip-card-header class="text-center"> <div class="bg-primary/10 text-primary mx-auto mb-2 flex size-12 items-center justify-center rounded-full" > ${icon(ShieldCheck, 'shield-check', 'size-6')} </div> <uip-card-title class="text-2xl">${this.heading}</uip-card-title> <uip-card-description>${this.description}</uip-card-description> </uip-card-header> <uip-card-content class="space-y-4"> <div class="flex justify-center"> <uip-pin-input .value=${this.code} ?disabled=${this.verifying} @input=${this.onCodeInput} ></uip-pin-input> </div> ${this.verifying ? html`<p class="text-muted-foreground text-center text-sm">Verifying…</p>` : nothing} ${this.error ? html`<p class="text-destructive text-center text-sm">${this.error}</p>` : nothing} <div class="text-center"> ${this.resendIn === 0 ? html`<button type="button" class="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline-offset-4 hover:underline" @click=${this.startResendCooldown} > ${icon(RotateCw, 'rotate-cw', 'size-3')} Resend code </button>` : html`<p class="text-muted-foreground text-xs">Resend available in ${this.resendIn}s</p>`} </div> </uip-card-content> <uip-card-footer class="justify-center"> <p class="text-muted-foreground text-xs"> Lost your device? <a href=${this.recoveryHref} class="text-foreground underline-offset-4 hover:underline"> Use a recovery code </a> </p> </uip-card-footer> ` : html` <uip-card-content class="space-y-4 pt-6 text-center"> <div class="bg-success/10 text-success mx-auto flex size-12 items-center justify-center rounded-full" > ${icon(ShieldCheck, 'shield-check', 'size-6')} </div> <div class="space-y-1"> <h3 class="text-lg font-semibold">Verified</h3> <p class="text-muted-foreground text-sm">You're all set. Continuing to your dashboard…</p> </div> <a href=${this.continueHref} @click=${() => this.dispatchEvent(new CustomEvent('continue', { bubbles: true, composed: true }))} > <uip-button class="w-full">Continue</uip-button> </a> </uip-card-content> `} </uip-card> </div> ` } } customElements.get('uip-auth-mfa') || customElements.define('uip-auth-mfa', UipAuthMfa) declare global { interface HTMLElementTagNameMap { 'uip-auth-mfa': UipAuthMfa } }
Raw manifest:https://uipkge.dev/r/lit/auth-mfa.json