ui(auth): 3-card sign-in chooser (extension / signer / phone) + lockfile note

LoginDialog leads with a three-card chooser (Browser extension, Signer =
Bunker on desktop / Amber on Android, Sign in with your phone) and a quiet
Secret Key link; the NostrConnect QR moved to its own connect panel. The
desktop welcome modal opens on this. Verified with a headless 1280x900
screenshot. Also records why the full npm update was reverted (duplicate
transitive copies + TS bump) and that the remaining fixes are majors.
This commit is contained in:
SkateSpots 2026-10-26 11:12:00 +07:00
parent aa2a70e9fd
commit 2164dd61b0
2 changed files with 70 additions and 22 deletions

View file

@ -1527,10 +1527,15 @@ session with **no other memory of this project**. So:
`min-width: 1024px`, after ~900 ms) as a welcome/sign-in offer; dismissal is
remembered in `skatespots:desktop-welcome-dismissed`, and it also offers the
normal `SignupDialog`. It reuses the existing methods (browser extension,
bunker/signer, Amber on Android, nsec, and the **NostrConnect QR**), which is
what the reference screenshot showed — the dialog is still **tabbed**, not
the reference's 3-card layout (a restyle would need a visual QA pass).
⚠️ `SignupDialog` is a **default-only** export, so lazy-load it as
bunker/signer, Amber on Android, nsec, and the **NostrConnect QR**).
**3-card restyle (2026-10-26):** `LoginDialog` now leads with a **three-card
chooser** — Browser extension / Signer (Bunker on desktop, Amber on Android) /
**Sign in with your phone** (NostrConnect) — plus a quiet **Secret Key** link;
the cards are a **controlled tab chooser** (`loginTab` state) over the existing
panels, and the NostrConnect QR moved from the bunker tab into its own
`connect` panel. Verified with a headless 1280×900 screenshot of the
auto-opened welcome modal. ⚠️ `SignupDialog` is a **default-only** export, so
lazy-load it as
`lazy(() => import('…/SignupDialog'))`.
- **First-run checklist + backup-reminder gating (fixed 2026-10-12).** Two
onboarding prompts nagged returning users:
@ -2288,6 +2293,17 @@ Rules:
`xlsx@0.18.5` (two high; **no fix published**) and `zod` (patch). Dev-dep CVEs
don't ship to users; `xlsx` is lazy-loaded for exports. Run `--force` to apply
the fixable ones (gated on tests).
⚠️ **`--force` on 2026-10-26 failed the gate and auto-reverted:** the blind
`npm update` cut OSV findings 107 → 13 but produced **duplicate transitive
copies** (`@tanstack/query-core` nested under `react-query-persist-client`;
`@nostrify/nostrify`/`@nostrify/types` nested under `@nostrify/react`) and a
**TypeScript bump** (TS 5.9 typed-array errors in `stashBackup`/`usePushNotifications`),
so `tsc` failed. The remaining fixes are mostly **major** (vitest 4,
react-router 7, zod 4) plus **`xlsx@0.18.5` with no fix** (we only *write*
XLSX for export, so the parse-side CVEs likely don't apply — verify before
replacing). Lesson: do NOT rely on a blanket `npm update`; update in curated
batches (pin `typescript`, add `overrides`/`npm dedupe` for the duplicated
packages, keep majors as their own migrations) and let the timer gate each.
### Community relay — self-hosted Newlay (2026-10-12)

View file

@ -11,7 +11,7 @@ import { Shield, UserPlus, Sparkles, Cloud, Smartphone, Check, Loader2, KeyRound
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import { useLoginActions } from '@/hooks/useLoginActions';
import { useToast } from '@/hooks/useToast';
import { cn } from '@/lib/utils';
@ -64,6 +64,10 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
const { toast } = useToast();
const isAndroid = /Android/i.test(navigator.userAgent);
// Which login method panel is showing. The three method cards below switch it.
const [loginTab, setLoginTab] = useState<'amber' | 'bunker' | 'nsec' | 'extension' | 'connect'>(
isAndroid ? 'amber' : 'bunker',
);
// Step 1: open Amber
// - On Capacitor (ZapStore APK): use the native plugin — direct Promise, no redirects needed
@ -349,21 +353,46 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
<div className='flex-1 border-t border-gray-800' />
</div>
<Tabs defaultValue={isAndroid ? 'amber' : 'bunker'} className="w-full">
<TabsList className="grid w-full grid-cols-4 bg-black/50 border border-[#ff0040]/30 rounded-lg mb-4 h-auto p-1">
<TabsTrigger value="amber" className="flex items-center gap-1 text-xs py-2 px-1">
<Smartphone className="w-3 h-3" /><span>Amber</span>
</TabsTrigger>
<TabsTrigger value="bunker" className="flex items-center gap-1 text-xs py-2 px-1">
<Cloud className="w-3 h-3" /><span>Bunker</span>
</TabsTrigger>
<TabsTrigger value="nsec" className="flex items-center gap-1 text-xs py-2 px-1">
<KeyRound className="w-3 h-3" /><span>nsec</span>
</TabsTrigger>
<TabsTrigger value="extension" className="flex items-center gap-1 text-xs py-2 px-1">
<Shield className="w-3 h-3" /><span>Ext</span>
</TabsTrigger>
</TabsList>
<Tabs value={loginTab} onValueChange={(v) => setLoginTab(v as typeof loginTab)} className="w-full">
{/* Method cards — a clear three-way choice (the old 4-tab strip was
jargon-heavy). The secret-key option stays as a quiet link. */}
<div className="mb-3 grid grid-cols-3 gap-2">
{([
{ id: 'extension', Icon: Shield, label: t('settingsUi.loginDialog.browserExtensionTitle') },
{ id: isAndroid ? 'amber' : 'bunker', Icon: isAndroid ? Smartphone : Cloud, label: isAndroid ? t('settingsUi.loginDialog.loginWithAmber') : t('settingsUi.loginDialog.bunkerTitle') },
{ id: 'connect', Icon: QrCode, label: t('settingsUi.loginDialog.signInWithPhone') },
] as const).map(({ id, Icon, label }) => {
const active = loginTab === id;
return (
<button
key={id}
type="button"
onClick={() => setLoginTab(id)}
className={cn(
'flex min-h-0 flex-col items-center gap-1.5 rounded-xl border p-3 text-center transition-colors',
active
? 'border-[#ccff00]/60 bg-[#ccff00]/10 text-[#ccff00]'
: 'border-white/10 bg-black/40 text-gray-300 hover:border-white/25 hover:text-white',
)}
>
<Icon className="h-5 w-5" />
<span className="text-[11px] font-bold leading-tight">{label}</span>
</button>
);
})}
</div>
<div className="mb-3 text-center">
<button
type="button"
onClick={() => setLoginTab('nsec')}
className={cn(
'text-[11px] underline underline-offset-2',
loginTab === 'nsec' ? 'text-yellow-300' : 'text-gray-500 hover:text-gray-300',
)}
>
{t('settingsUi.loginDialog.secretKeyTitle')}
</button>
</div>
{/* ── AMBER TAB ── */}
<TabsContent value='amber' className='space-y-3'>
@ -476,8 +505,8 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
</TabsContent>
{/* ── BUNKER TAB ── */}
<TabsContent value='bunker' className='space-y-3'>
{/* ── NOSTR CONNECT TAB ── */}
<TabsContent value='connect' className='space-y-3'>
{/* Phone pairing — the canonical "log in on desktop with the key
already on your phone" flow. Client-generated URI; the signer
scans and pairs over a relay. */}
@ -545,7 +574,10 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
</div>
)}
</div>
</TabsContent>
{/* ── BUNKER TAB ── */}
<TabsContent value='bunker' className='space-y-3'>
<div className='relative flex items-center'>
<div className='flex-1 border-t border-gray-800' />
<span className='px-3 text-xs text-gray-500'>{t('settingsUi.loginDialog.orPasteBunker')}</span>