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
|
`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
|
remembered in `skatespots:desktop-welcome-dismissed`, and it also offers the
|
||||||
normal `SignupDialog`. It reuses the existing methods (browser extension,
|
normal `SignupDialog`. It reuses the existing methods (browser extension,
|
||||||
bunker/signer, Amber on Android, nsec, and the **NostrConnect QR**), which is
|
bunker/signer, Amber on Android, nsec, and the **NostrConnect QR**).
|
||||||
what the reference screenshot showed — the dialog is still **tabbed**, not
|
**3-card restyle (2026-10-26):** `LoginDialog` now leads with a **three-card
|
||||||
the reference's 3-card layout (a restyle would need a visual QA pass).
|
chooser** — Browser extension / Signer (Bunker on desktop, Amber on Android) /
|
||||||
⚠️ `SignupDialog` is a **default-only** export, so lazy-load it as
|
**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'))`.
|
`lazy(() => import('…/SignupDialog'))`.
|
||||||
- **First-run checklist + backup-reminder gating (fixed 2026-10-12).** Two
|
- **First-run checklist + backup-reminder gating (fixed 2026-10-12).** Two
|
||||||
onboarding prompts nagged returning users:
|
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
|
`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
|
don't ship to users; `xlsx` is lazy-loaded for exports. Run `--force` to apply
|
||||||
the fixable ones (gated on tests).
|
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)
|
### 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 { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
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 { useLoginActions } from '@/hooks/useLoginActions';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -64,6 +64,10 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
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
|
// Step 1: open Amber
|
||||||
// - On Capacitor (ZapStore APK): use the native plugin — direct Promise, no redirects needed
|
// - 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 className='flex-1 border-t border-gray-800' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue={isAndroid ? 'amber' : 'bunker'} className="w-full">
|
<Tabs value={loginTab} onValueChange={(v) => setLoginTab(v as typeof loginTab)} 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">
|
{/* Method cards — a clear three-way choice (the old 4-tab strip was
|
||||||
<TabsTrigger value="amber" className="flex items-center gap-1 text-xs py-2 px-1">
|
jargon-heavy). The secret-key option stays as a quiet link. */}
|
||||||
<Smartphone className="w-3 h-3" /><span>Amber</span>
|
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||||
</TabsTrigger>
|
{([
|
||||||
<TabsTrigger value="bunker" className="flex items-center gap-1 text-xs py-2 px-1">
|
{ id: 'extension', Icon: Shield, label: t('settingsUi.loginDialog.browserExtensionTitle') },
|
||||||
<Cloud className="w-3 h-3" /><span>Bunker</span>
|
{ id: isAndroid ? 'amber' : 'bunker', Icon: isAndroid ? Smartphone : Cloud, label: isAndroid ? t('settingsUi.loginDialog.loginWithAmber') : t('settingsUi.loginDialog.bunkerTitle') },
|
||||||
</TabsTrigger>
|
{ id: 'connect', Icon: QrCode, label: t('settingsUi.loginDialog.signInWithPhone') },
|
||||||
<TabsTrigger value="nsec" className="flex items-center gap-1 text-xs py-2 px-1">
|
] as const).map(({ id, Icon, label }) => {
|
||||||
<KeyRound className="w-3 h-3" /><span>nsec</span>
|
const active = loginTab === id;
|
||||||
</TabsTrigger>
|
return (
|
||||||
<TabsTrigger value="extension" className="flex items-center gap-1 text-xs py-2 px-1">
|
<button
|
||||||
<Shield className="w-3 h-3" /><span>Ext</span>
|
key={id}
|
||||||
</TabsTrigger>
|
type="button"
|
||||||
</TabsList>
|
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 ── */}
|
{/* ── AMBER TAB ── */}
|
||||||
<TabsContent value='amber' className='space-y-3'>
|
<TabsContent value='amber' className='space-y-3'>
|
||||||
|
|
@ -476,8 +505,8 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* ── BUNKER TAB ── */}
|
{/* ── NOSTR CONNECT TAB ── */}
|
||||||
<TabsContent value='bunker' className='space-y-3'>
|
<TabsContent value='connect' className='space-y-3'>
|
||||||
{/* Phone pairing — the canonical "log in on desktop with the key
|
{/* Phone pairing — the canonical "log in on desktop with the key
|
||||||
already on your phone" flow. Client-generated URI; the signer
|
already on your phone" flow. Client-generated URI; the signer
|
||||||
scans and pairs over a relay. */}
|
scans and pairs over a relay. */}
|
||||||
|
|
@ -545,7 +574,10 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLog
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ── BUNKER TAB ── */}
|
||||||
|
<TabsContent value='bunker' className='space-y-3'>
|
||||||
<div className='relative flex items-center'>
|
<div className='relative flex items-center'>
|
||||||
<div className='flex-1 border-t border-gray-800' />
|
<div className='flex-1 border-t border-gray-800' />
|
||||||
<span className='px-3 text-xs text-gray-500'>{t('settingsUi.loginDialog.orPasteBunker')}</span>
|
<span className='px-3 text-xs text-gray-500'>{t('settingsUi.loginDialog.orPasteBunker')}</span>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue