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:
parent
aa2a70e9fd
commit
2164dd61b0
2 changed files with 70 additions and 22 deletions
24
AGENTS.md
24
AGENTS.md
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue