
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Two-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.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/auth-mfa.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/auth-mfa.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/auth-mfa.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/auth-mfa.jsonnpx shadcn-vue@latest add @uipkge/auth-mfaInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
title | string | 'Two-step verification' | optional |
description | string | 'Enter the 6-digit code from your authenticator app.' | optional |
continueHref | string | '/' | optional |
recoveryHref | string | '#' | optional |
demoCode | string | '123456' | optional |
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ShieldCheck, RotateCw } from 'lucide-vue-next'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { PinInput, PinInputGroup, PinInputSlot } from '@/components/ui/pin-input'
const props = withDefaults(
defineProps<{
title?: string
description?: string
continueHref?: string
recoveryHref?: string
demoCode?: string
}>(),
{
title: 'Two-step verification',
description: 'Enter the 6-digit code from your authenticator app.',
continueHref: '/',
recoveryHref: '#',
demoCode: '123456',
},
)
const emit = defineEmits<{
(e: 'verify', code: string): void
(e: 'resend'): void
(e: 'continue'): void
}>()
const code = ref<string[]>([])
const verifying = ref(false)
const verified = ref(false)
const error = ref('')
const resendIn = ref(0)
const joined = computed(() => code.value.join(''))
watch(joined, (val) => {
if (val.length === 6) {
error.value = ''
verifying.value = true
emit('verify', val)
setTimeout(() => {
verifying.value = false
if (val === props.demoCode) verified.value = true
else {
error.value = `Invalid code. Try ${props.demoCode} for the demo.`
code.value = []
}
}, 700)
}
})
function startResendCooldown() {
resendIn.value = 30
emit('resend')
const t = setInterval(() => {
resendIn.value -= 1
if (resendIn.value <= 0) clearInterval(t)
}, 1000)
}
</script>
<template>
<div data-slot="auth-mfa" class="bg-background flex min-h-svh items-center justify-center p-6">
<Card class="w-full max-w-sm">
<template v-if="!verified">
<CardHeader class="text-center">
<div class="bg-primary/10 text-primary mx-auto mb-2 flex size-12 items-center justify-center rounded-full">
<ShieldCheck class="size-6" />
</div>
<CardTitle class="text-2xl">{{ title }}</CardTitle>
<CardDescription>{{ description }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="flex justify-center">
<PinInput v-model="code" otp :disabled="verifying">
<PinInputGroup>
<PinInputSlot v-for="i in 6" :key="i" :index="i - 1" />
</PinInputGroup>
</PinInput>
</div>
<p v-if="verifying" class="text-muted-foreground text-center text-sm">Verifying…</p>
<p v-if="error" class="text-destructive text-center text-sm">{{ error }}</p>
<div class="text-center">
<button
v-if="resendIn === 0"
type="button"
class="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline-offset-4 hover:underline"
@click="startResendCooldown"
>
<RotateCw class="size-3" />Resend code
</button>
<p v-else class="text-muted-foreground text-xs">Resend available in {{ resendIn }}s</p>
</div>
</CardContent>
<CardFooter class="justify-center">
<p class="text-muted-foreground text-xs">
Lost your device?
<a :href="recoveryHref" class="text-foreground underline-offset-4 hover:underline">Use a recovery code</a>
</p>
</CardFooter>
</template>
<template v-else>
<CardContent 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">
<ShieldCheck class="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="continueHref" @click="emit('continue')"><Button class="w-full">Continue</Button></a>
</CardContent>
</template>
</Card>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/auth-mfa.json