Build passkey add wizard UI

Add a new AddPasskeyWizard component and wire it into SecurityPage.
- Introduced AddPasskeyWizard.tsx to guide users through naming, registering, and confirming a passkey with a simulated flow.
- Integrated wizard into SecurityPage: removed inline placeholder and wired AddPasskeyWizard with stateful open/close handling.
- Replaced previous inline UI to use the new wizard for adding passkeys.

X-Lovable-Edit-ID: edt-32783fa6-048e-4efe-898a-544ef48530d1
This commit is contained in:
gpt-engineer-app[bot]
2026-01-06 16:20:09 +00:00
2 changed files with 198 additions and 4 deletions
@@ -0,0 +1,184 @@
import { useState } from "react";
import { Fingerprint, Loader2, CheckCircle, AlertCircle } from "lucide-react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
interface AddPasskeyWizardProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: (passkey: { id: string; name: string }) => void;
}
type WizardStep = "name" | "registering" | "success" | "error";
export function AddPasskeyWizard({ open, onOpenChange, onSuccess }: AddPasskeyWizardProps) {
const [step, setStep] = useState<WizardStep>("name");
const [passkeyName, setPasskeyName] = useState("");
const [error, setError] = useState<string | null>(null);
const handleStartRegistration = async () => {
if (!passkeyName.trim()) return;
setStep("registering");
setError(null);
try {
// Simulate WebAuthn registration flow
// In production, this would call the backend to get challenge options,
// then call navigator.credentials.create()
await new Promise((resolve) => setTimeout(resolve, 2000));
// Simulate success
setStep("success");
// Notify parent after a short delay
setTimeout(() => {
onSuccess?.({ id: crypto.randomUUID(), name: passkeyName.trim() });
}, 1500);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to register passkey");
setStep("error");
}
};
const handleClose = () => {
onOpenChange(false);
// Reset state after dialog closes
setTimeout(() => {
setStep("name");
setPasskeyName("");
setError(null);
}, 200);
};
const handleRetry = () => {
setStep("name");
setError(null);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Fingerprint className="w-5 h-5" />
Add Passkey
</DialogTitle>
<DialogDescription>
{step === "name" && "Register a passkey for passwordless sign-in"}
{step === "registering" && "Follow your browser's prompts to register"}
{step === "success" && "Passkey registered successfully"}
{step === "error" && "Registration failed"}
</DialogDescription>
</DialogHeader>
<div className="py-4">
{step === "name" && (
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="passkeyName">Passkey name</Label>
<Input
id="passkeyName"
placeholder="e.g., MacBook Pro Touch ID"
value={passkeyName}
onChange={(e) => setPasskeyName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && passkeyName.trim()) {
handleStartRegistration();
}
}}
/>
<p className="text-xs text-muted-foreground">
Give this passkey a name to help you identify it later
</p>
</div>
<div className="rounded-lg border bg-muted/50 p-4 space-y-2">
<p className="text-sm font-medium">What happens next</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Your browser will prompt you to authenticate</li>
<li> Use Touch ID, Face ID, or your security key</li>
<li> The passkey will be stored on this device</li>
</ul>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button
onClick={handleStartRegistration}
disabled={!passkeyName.trim()}
>
Continue
</Button>
</div>
</div>
)}
{step === "registering" && (
<div className="flex flex-col items-center py-8 space-y-4">
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center animate-pulse">
<Fingerprint className="w-8 h-8 text-primary" />
</div>
<div className="text-center space-y-2">
<p className="font-medium">Waiting for authentication</p>
<p className="text-sm text-muted-foreground">
Follow the prompts from your browser or device
</p>
</div>
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
</div>
)}
{step === "success" && (
<div className="flex flex-col items-center py-8 space-y-4">
<div className="w-16 h-16 rounded-full bg-success/10 flex items-center justify-center">
<CheckCircle className="w-8 h-8 text-success" />
</div>
<div className="text-center space-y-2">
<p className="font-medium">Passkey added</p>
<p className="text-sm text-muted-foreground">
"{passkeyName}" is now registered and ready to use
</p>
</div>
<Button onClick={handleClose} className="mt-2">
Done
</Button>
</div>
)}
{step === "error" && (
<div className="flex flex-col items-center py-8 space-y-4">
<div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center">
<AlertCircle className="w-8 h-8 text-destructive" />
</div>
<div className="text-center space-y-2">
<p className="font-medium">Registration failed</p>
<p className="text-sm text-muted-foreground">
{error || "Unable to register passkey. Please try again."}
</p>
</div>
<div className="flex gap-2 mt-2">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleRetry}>
Try again
</Button>
</div>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}
+13 -3
View File
@@ -1,14 +1,15 @@
import { useState } from "react"; import { useState } from "react";
import { Lock, Fingerprint, Smartphone, Shield, Plus, AlertTriangle, CheckCircle } from "lucide-react"; import { Lock, Fingerprint, Smartphone, Shield, Plus, CheckCircle } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch"; import { AddPasskeyWizard } from "@/components/security/AddPasskeyWizard";
export default function SecurityPage() { export default function SecurityPage() {
const [showPasswordForm, setShowPasswordForm] = useState(false); const [showPasswordForm, setShowPasswordForm] = useState(false);
const [showAddPasskey, setShowAddPasskey] = useState(false);
// Mock security data // Mock security data
const security = { const security = {
@@ -149,7 +150,7 @@ export default function SecurityPage() {
Use biometrics or security keys for passwordless login Use biometrics or security keys for passwordless login
</CardDescription> </CardDescription>
</div> </div>
<Button size="sm"> <Button size="sm" onClick={() => setShowAddPasskey(true)}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add passkey Add passkey
</Button> </Button>
@@ -180,6 +181,15 @@ export default function SecurityPage() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<AddPasskeyWizard
open={showAddPasskey}
onOpenChange={setShowAddPasskey}
onSuccess={(passkey) => {
console.log("Passkey added:", passkey);
setShowAddPasskey(false);
}}
/>
</div> </div>
); );
} }