diff --git a/package.json b/package.json index a1b9e32..877ea2a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "skate-spots", "private": true, - "version": "1.0.30", + "version": "1.0.31", "type": "module", "homepage": "https://skate-spots.com", "scripts": { diff --git a/release-notes.txt b/release-notes.txt index 8cc8483..2f11c1d 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,6 +1,5 @@ -v1.0.30 — Amber login fixed definitively +v1.0.31 — Amber login redesigned for GrapheneOS -- Switched to correct Android intent: URL format per Amber README -- Removed # from callbackUrl (# breaks the intent: format which uses # as delimiter) -- Added pre-redirect in main.tsx: when Amber opens /amber-callback, app redirects to /#/amber-callback so HashRouter can match the route -- All params passed raw (no encoding) inside intent: string as required +- GrapheneOS Vanadium blocks intent: URLs from web pages (security hardening) +- New flow: tap Open Amber → approve in Amber → app tries clipboard auto-read → if denied, shows paste input for hex pubkey +- Works on all Android browsers including GrapheneOS Vanadium diff --git a/src/components/auth/LoginDialog.tsx b/src/components/auth/LoginDialog.tsx index 08e5dc4..5795153 100644 --- a/src/components/auth/LoginDialog.tsx +++ b/src/components/auth/LoginDialog.tsx @@ -1,7 +1,7 @@ // NOTE: This file is stable and usually should not be modified. // It is important that all functionality in this file is preserved, and should only be modified if explicitly requested. -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import { Shield, UserPlus, Sparkles, Cloud, Smartphone, Check, Loader2, KeyRound } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -10,7 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; -import { openAmberForLogin } from '@/lib/amber'; + interface LoginDialogProps { isOpen: boolean; @@ -26,83 +26,73 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog const [nsecInput, setNsecInput] = useState(''); const [nsecError, setNsecError] = useState(''); - // Amber NIP-55 state — just waiting for callback, no relay nonsense - const [amberWaiting, setAmberWaiting] = useState(false); - const [amberDone, setAmberDone] = useState(false); - const pollRef = useRef | null>(null); + // Amber NIP-55 state + const [amberStep, setAmberStep] = useState<'idle' | 'waiting' | 'paste' | 'done'>('idle'); + const [pubkeyInput, setPubkeyInput] = useState(''); + const [pubkeyError, setPubkeyError] = useState(''); const login = useLoginActions(); const { toast } = useToast(); const isAndroid = /Android/i.test(navigator.userAgent); - // Poll localStorage for the pubkey that AmberCallbackPage writes when Amber returns - const startPolling = () => { - if (pollRef.current) clearInterval(pollRef.current); - pollRef.current = setInterval(() => { - const pubkey = localStorage.getItem('amber_pending_pubkey'); - if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey)) { - localStorage.removeItem('amber_pending_pubkey'); - clearInterval(pollRef.current!); - pollRef.current = null; - console.log('[Amber] Got pubkey from callback:', pubkey); - try { - login.amber(pubkey); - setAmberDone(true); - setAmberWaiting(false); - toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); - setTimeout(() => { onLogin(); onClose(); }, 800); - } catch (e) { - console.error('[Amber] login.amber() failed:', e); - setAmberWaiting(false); - toast({ title: 'Login failed', description: 'Could not log in with Amber. Try again.', variant: 'destructive' }); - } - } - }, 500); - }; - + // Step 1: open Amber asking it to copy pubkey to clipboard (no callback URL) const handleAmberLogin = () => { - console.log('[Amber] Opening Amber for NIP-55 get_public_key...'); - localStorage.removeItem('amber_pending_pubkey'); // clear any stale value - openAmberForLogin(); - setAmberWaiting(true); - startPolling(); + // nostrsigner: with no callbackUrl → Amber copies pubkey to clipboard + // This works on ALL Android browsers including GrapheneOS Vanadium + window.location.href = 'intent:#Intent;scheme=nostrsigner;S.compressionType=none;S.returnType=signature;S.type=get_public_key;end'; + setAmberStep('waiting'); }; - // Also detect when user comes back to the app (visibilitychange) + // Step 2: user comes back — try clipboard automatically, else show paste input useEffect(() => { - if (!amberWaiting) return; - const handleVisible = () => { - if (document.visibilityState === 'visible') { - console.log('[Amber] App foregrounded — checking for pubkey'); - const pubkey = localStorage.getItem('amber_pending_pubkey'); - if (pubkey && /^[0-9a-f]{64}$/i.test(pubkey)) { - localStorage.removeItem('amber_pending_pubkey'); - if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } - try { - login.amber(pubkey); - setAmberDone(true); - setAmberWaiting(false); - toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); - setTimeout(() => { onLogin(); onClose(); }, 800); - } catch (e) { - console.error('[Amber] login.amber() failed:', e); - setAmberWaiting(false); - toast({ title: 'Login failed', description: 'Could not log in with Amber. Try again.', variant: 'destructive' }); - } + if (amberStep !== 'waiting') return; + const handleVisible = async () => { + if (document.visibilityState !== 'visible') return; + // Try reading clipboard automatically + try { + const text = await navigator.clipboard.readText(); + const clean = text.trim().toLowerCase(); + if (/^[0-9a-f]{64}$/.test(clean)) { + completeLogin(clean); + return; } - } + } catch { /* clipboard permission denied — fall through to paste UI */ } + // Show paste input + setAmberStep('paste'); }; document.addEventListener('visibilitychange', handleVisible); return () => document.removeEventListener('visibilitychange', handleVisible); - }, [amberWaiting, login, onLogin, onClose, toast]); + }, [amberStep]); + + const completeLogin = (pubkey: string) => { + try { + login.amber(pubkey); + setAmberStep('done'); + toast({ title: '🛹 Logged in!', description: 'Connected via Amber!' }); + setTimeout(() => { onLogin(); onClose(); }, 800); + } catch (e) { + console.error('[Amber] login.amber() failed:', e); + setAmberStep('idle'); + toast({ title: 'Login failed', description: 'Could not log in with Amber. Try again.', variant: 'destructive' }); + } + }; + + const handlePubkeySubmit = () => { + const clean = pubkeyInput.trim().toLowerCase(); + if (!/^[0-9a-f]{64}$/.test(clean)) { + setPubkeyError('Must be a 64-character hex pubkey'); + return; + } + completeLogin(clean); + }; // Reset when dialog closes useEffect(() => { if (!isOpen) { - if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } - setAmberWaiting(false); - setAmberDone(false); + setAmberStep('idle'); + setPubkeyInput(''); + setPubkeyError(''); setBunkerUri(''); setBunkerError(''); setNsecInput(''); @@ -191,8 +181,9 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog {/* ── AMBER TAB ── */} - {/* Success */} - {amberDone && ( + + {/* Done */} + {amberStep === 'done' && (

Logged in!

@@ -200,45 +191,76 @@ export const LoginDialog: React.FC = ({ isOpen, onClose, onLog
)} - {/* Waiting for Amber to return */} - {!amberDone && amberWaiting && ( -
- -
-

Waiting for Amber...

-

Approve in Amber, then come back here.

-
- -
- )} - - {/* Idle — show login button */} - {!amberDone && !amberWaiting && ( + {/* Step 1 idle — show login button */} + {amberStep === 'idle' && (

Login with Amber

-

NIP-55 Android signer. Tap, approve in Amber, done.

+

Opens Amber → approve → come back here.

)} + + {/* Step 2 waiting — app just navigated to Amber */} + {amberStep === 'waiting' && ( +
+ +
+

Approve in Amber

+

Tap "Get public key" in Amber, then come back here.

+
+ + +
+ )} + + {/* Step 3 paste — clipboard read failed or user chose manual */} + {amberStep === 'paste' && ( +
+
+ +

Paste your pubkey

+

+ In Amber: tap your profile → copy public key (hex). Paste it below. +

+
+
+ { setPubkeyInput(e.target.value); setPubkeyError(''); }} + className='bg-black/60 border-[#ccff00]/30 text-white placeholder:text-gray-600 text-xs font-mono' + autoComplete='off' + autoCorrect='off' + spellCheck={false} + /> + {pubkeyError &&

{pubkeyError}

} +
+ + +
+ )} +
{/* ── BUNKER TAB ── */}