feat: forced PIN setup modal on all authenticated pages
All checks were successful
CI / ci (pull_request) Successful in 29s
CI / e2e (pull_request) Successful in 1m7s

Replaces the alert banner with a blocking modal dialog that requires
users to set a PIN before they can use the app. Cannot be dismissed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-05 20:01:14 +00:00
parent 2cd646ddea
commit 924a28e201

View File

@@ -1,5 +1,5 @@
import { createFileRoute, Outlet, Link, redirect, useRouter } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
import { queryOptions } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { api } from '@/lib/api-client'
@@ -8,8 +8,10 @@ import { myPermissionsOptions } from '@/api/rbac'
import { moduleListOptions } from '@/api/modules'
import { Avatar } from '@/components/shared/avatar-upload'
import { Button } from '@/components/ui/button'
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings, PanelLeftClose, PanelLeft, CalendarDays, GraduationCap, CalendarRange, BookOpen, BookMarked, Package2, Tag, Truck, ShoppingCart, AlertTriangle } from 'lucide-react'
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert'
import { Users, UserRound, HelpCircle, Shield, UserCog, LogOut, User, Wrench, Package, ClipboardList, FolderOpen, KeyRound, Settings, PanelLeftClose, PanelLeft, CalendarDays, GraduationCap, CalendarRange, BookOpen, BookMarked, Package2, Tag, Truck, ShoppingCart } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { toast } from 'sonner'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: () => {
@@ -105,6 +107,58 @@ function NavGroup({ label, children, collapsed }: { label: string; children: Rea
)
}
function SetPinModal() {
const queryClient = useQueryClient()
const [pin, setPin] = useState('')
const [confirmPin, setConfirmPin] = useState('')
const [error, setError] = useState('')
const setPinMutation = useMutation({
mutationFn: () => api.post('/v1/auth/set-pin', { pin }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['auth', 'me'] })
toast.success('PIN set successfully')
},
onError: (err) => setError(err.message),
})
function handleSubmit(e: React.FormEvent) {
e.preventDefault()
setError('')
if (pin.length < 4 || pin.length > 6) { setError('PIN must be 4-6 digits'); return }
if (!/^\d+$/.test(pin)) { setError('PIN must be digits only'); return }
if (pin !== confirmPin) { setError('PINs do not match'); return }
setPinMutation.mutate()
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<div className="w-full max-w-sm rounded-xl border bg-card p-6 shadow-xl">
<h2 className="text-lg font-semibold mb-1">Set your POS PIN</h2>
<p className="text-sm text-muted-foreground mb-4">
A PIN is required to use the Point of Sale. Choose a 4-6 digit PIN you'll use to unlock the terminal.
</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>PIN</Label>
<Input type="password" inputMode="numeric" maxLength={6} value={pin} onChange={(e) => setPin(e.target.value)} placeholder="****" autoFocus />
</div>
<div className="space-y-2">
<Label>Confirm PIN</Label>
<Input type="password" inputMode="numeric" maxLength={6} value={confirmPin} onChange={(e) => setConfirmPin(e.target.value)} placeholder="****" />
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<Button type="submit" className="w-full" disabled={setPinMutation.isPending}>
{setPinMutation.isPending ? 'Setting...' : 'Set PIN'}
</Button>
</form>
</div>
</div>
)
}
function AuthenticatedLayout() {
const router = useRouter()
const user = useAuthStore((s) => s.user)
@@ -271,18 +325,7 @@ function AuthenticatedLayout() {
</div>
</nav>
<main className="flex-1 p-6 min-h-screen">
{profile && !profile.hasPin && (
<Alert className="mb-4">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>POS PIN not set</AlertTitle>
<AlertDescription>
You need a PIN to use the Point of Sale.{' '}
<Link to="/profile" search={{ tab: 'security' }} className="underline font-medium text-foreground">
Set your PIN
</Link>
</AlertDescription>
</Alert>
)}
{profile && !profile.hasPin && <SetPinModal />}
<Outlet />
</main>
</div>