fix: Amber Capacitor APK + mobile UI improvements
Amber login (critical): - Replace window.location.href with window.open(_system) in Capacitor WebViews - Capacitor's WebView throws "malformed nostrsigner request" on href navigation - New openAmberUrl() helper detects Capacitor via window.Capacitor.isNativePlatform() - Add openAmberForLoginNoCallback() for PWA/Capacitor clipboard flow - Fix all 6 Amber URL opens in amber.ts + LoginDialog.tsx Mobile nav: - Newsletter and Sasquatch AI (/pyramid) added to hamburger menu - Sasquatch floating guide now shows on mobile (removed hidden md:block) Lab page mobile: - Action buttons (Offer Coaching/Filming/Editing/Get Card) now 2-col grid on mobile - Service type filter (Coaches/Filmers/Editors) now 3-col grid, full width - Reduced top padding on mobile (py-6 → py-12 on md+) - Text truncates cleanly, icons don't overflow Wallet page mobile: - Reduced padding (py-6 md:py-8) - Tab triggers use h-auto + py-2.5 to avoid overflow - Smaller header text on mobile Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
576df73a6b
commit
a677e8687d
5 changed files with 105 additions and 68 deletions
|
|
@ -45,6 +45,8 @@ import {
|
|||
Zap,
|
||||
Heart,
|
||||
CreditCard,
|
||||
Radio,
|
||||
Bot,
|
||||
} from 'lucide-react';
|
||||
import { OfflineIndicator } from '@/components/OfflineIndicator';
|
||||
import { UserStatusIndicator } from '@/components/UserStatusIndicator';
|
||||
|
|
@ -680,6 +682,20 @@ export function Layout({ children }: LayoutProps) {
|
|||
isActive={isActive('/supporters')}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
<NavLink
|
||||
href="/newsletter"
|
||||
label="Newsletter"
|
||||
icon={Radio}
|
||||
isActive={isActive('/newsletter')}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
<NavLink
|
||||
href="/pyramid"
|
||||
label="Sasquatch AI"
|
||||
icon={Bot}
|
||||
isActive={isActive('/pyramid')}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
/>
|
||||
<NavLink
|
||||
href="/settings"
|
||||
label={t('nav.settings')}
|
||||
|
|
@ -811,13 +827,11 @@ export function Layout({ children }: LayoutProps) {
|
|||
t={t}
|
||||
/>
|
||||
|
||||
{/* Sasquatch Guide (Desktop only) */}
|
||||
<div className="hidden md:block">
|
||||
<SasquatchGuide
|
||||
message={currentMessage}
|
||||
onDismiss={dismissCurrentMessage}
|
||||
/>
|
||||
</div>
|
||||
{/* Sasquatch Guide (all screens) */}
|
||||
<SasquatchGuide
|
||||
message={currentMessage}
|
||||
onDismiss={dismissCurrentMessage}
|
||||
/>
|
||||
|
||||
{/* Set Status Dialog */}
|
||||
<SetStatusDialog
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
|
|||
import { useLoginActions } from '@/hooks/useLoginActions';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { subscribeToAmberReturn } from '@/lib/amber';
|
||||
import { subscribeToAmberReturn, openAmberForLogin, openAmberForLoginNoCallback } from '@/lib/amber';
|
||||
|
||||
interface LoginDialogProps {
|
||||
isOpen: boolean;
|
||||
|
|
@ -96,16 +96,13 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
|||
|| ('standalone' in window.navigator && (window.navigator as { standalone?: boolean }).standalone === true);
|
||||
|
||||
if (isPWA) {
|
||||
// ── PWA mode ──────────────────────────────────────────────────────────
|
||||
// When installed to home screen, the app is single-tab. Opening
|
||||
// nostrsigner: with a callbackUrl sends the callback to the browser,
|
||||
// not back to the PWA — so the PWA never sees the result.
|
||||
//
|
||||
// Instead: open Amber WITHOUT a callbackUrl. Amber copies the pubkey
|
||||
// to clipboard when there's no callback. When the user comes back,
|
||||
// the visibilitychange fires and we auto-read the clipboard.
|
||||
const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key';
|
||||
window.location.href = amberUrl;
|
||||
// ── PWA / Capacitor mode ───────────────────────────────────────────────
|
||||
// When installed to home screen or running as a Capacitor APK, the app
|
||||
// is single-tab. Use openAmberForLogin() which handles Capacitor WebViews
|
||||
// correctly via window.open(_system) instead of window.location.href.
|
||||
// Amber copies the pubkey to clipboard when there's no callback.
|
||||
// When the user comes back, visibilitychange fires and we read the clipboard.
|
||||
openAmberForLoginNoCallback();
|
||||
|
||||
// When user returns to the PWA after approving in Amber, auto-read clipboard
|
||||
unsubAmberRef.current = subscribeToAmberReturn(async () => {
|
||||
|
|
@ -166,9 +163,8 @@ const LoginDialog: React.FC<LoginDialogProps> = ({ isOpen, onClose, onLogin, onS
|
|||
unsubAmberRef.current = null;
|
||||
});
|
||||
|
||||
const callbackUrl = `${window.location.origin}/amber-callback?event=`;
|
||||
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
window.location.href = amberUrl;
|
||||
// Browser (tab) mode — use the callback URL flow
|
||||
openAmberForLogin();
|
||||
};
|
||||
|
||||
const handleBunkerLogin = async () => {
|
||||
|
|
|
|||
|
|
@ -246,6 +246,32 @@ function buildCallbackUrl(): string {
|
|||
return `${window.location.origin}/amber-callback?event=`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to an Amber nostrsigner: URL.
|
||||
* In Capacitor WebViews, window.location.href causes a "malformed request"
|
||||
* error because the WebView tries to navigate instead of firing an intent.
|
||||
* Use window.open() with _system target to hand off to Android properly.
|
||||
*/
|
||||
function openAmberUrl(url: string): void {
|
||||
// Capacitor / Android WebView: use _system to fire the intent
|
||||
const isCapacitor = !!(window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor?.isNativePlatform?.();
|
||||
if (isCapacitor) {
|
||||
window.open(url, '_system');
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Amber for get_public_key WITHOUT a callback URL.
|
||||
* Used in PWA/Capacitor mode: Amber copies pubkey to clipboard,
|
||||
* the app reads it on visibilitychange.
|
||||
*/
|
||||
export function openAmberForLoginNoCallback(): void {
|
||||
const amberUrl = 'nostrsigner:?compressionType=none&returnType=signature&type=get_public_key';
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open Amber for get_public_key operation.
|
||||
* Uses a dedicated /amber-callback route so the app state is preserved
|
||||
|
|
@ -267,7 +293,7 @@ export function openAmberForLogin(): void {
|
|||
const amberUrl = `nostrsigner:?compressionType=none&returnType=signature&type=get_public_key&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
console.log('[Amber] Opening Amber for login, callback:', callbackUrl);
|
||||
window.location.href = amberUrl;
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -309,7 +335,7 @@ export function openAmberForSignEvent(event: Omit<NostrEvent, 'id' | 'sig'>): vo
|
|||
const encodedEvent = encodeURIComponent(eventJson);
|
||||
const amberUrl = `nostrsigner:${encodedEvent}?compressionType=none&returnType=event&type=sign_event&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
window.location.href = amberUrl;
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -330,7 +356,7 @@ export function openAmberForNip04Encrypt(pubkey: string, plaintext: string): voi
|
|||
const encodedText = encodeURIComponent(plaintext);
|
||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
window.location.href = amberUrl;
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -351,7 +377,7 @@ export function openAmberForNip04Decrypt(pubkey: string, ciphertext: string): vo
|
|||
const encodedText = encodeURIComponent(ciphertext);
|
||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip04_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
window.location.href = amberUrl;
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -372,7 +398,7 @@ export function openAmberForNip44Encrypt(pubkey: string, plaintext: string): voi
|
|||
const encodedText = encodeURIComponent(plaintext);
|
||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_encrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
window.location.href = amberUrl;
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -393,7 +419,7 @@ export function openAmberForNip44Decrypt(pubkey: string, ciphertext: string): vo
|
|||
const encodedText = encodeURIComponent(ciphertext);
|
||||
const amberUrl = `nostrsigner:${encodedText}?pubkey=${pubkey}&compressionType=none&returnType=signature&type=nip44_decrypt&callbackUrl=${encodeURIComponent(callbackUrl)}`;
|
||||
|
||||
window.location.href = amberUrl;
|
||||
openAmberUrl(amberUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -102,8 +102,8 @@ export default function LabPage() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container px-4 py-12">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8">
|
||||
<div className="container px-4 py-6 md:py-12">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-6 md:mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-black uppercase mb-2 neon-header">
|
||||
<span className="neon-red">THE</span> <span className="neon-orange">LAB</span>
|
||||
|
|
@ -111,29 +111,29 @@ export default function LabPage() {
|
|||
<p className="text-gray-400">{t('labPage.subtitle')}</p>
|
||||
</div>
|
||||
{user ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Link to="/become-coach">
|
||||
<Button className="bg-red-500 hover:bg-red-600 gap-2 uppercase font-black">
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('labPage.offerCoaching')}
|
||||
<div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2 w-full md:w-auto">
|
||||
<Link to="/become-coach" className="col-span-1">
|
||||
<Button className="w-full bg-red-500 hover:bg-red-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{t('labPage.offerCoaching')}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/become-filmer">
|
||||
<Button className="bg-orange-500 hover:bg-orange-600 gap-2 uppercase font-black">
|
||||
<Plus className="h-4 w-4" />
|
||||
{t('labPage.offerFilming')}
|
||||
<Link to="/become-filmer" className="col-span-1">
|
||||
<Button className="w-full bg-orange-500 hover:bg-orange-600 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">{t('labPage.offerFilming')}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/become-editor">
|
||||
<Button className="bg-purple-600 hover:bg-purple-700 gap-2 uppercase font-black">
|
||||
<Plus className="h-4 w-4" />
|
||||
Offer Editing
|
||||
<Link to="/become-editor" className="col-span-1">
|
||||
<Button className="w-full bg-purple-600 hover:bg-purple-700 gap-1.5 uppercase font-black text-xs sm:text-sm">
|
||||
<Plus className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">Offer Editing</span>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/card">
|
||||
<Button variant="outline" className="gap-2 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10">
|
||||
<CreditCard className="h-4 w-4" />
|
||||
Get Your Card
|
||||
<Link to="/card" className="col-span-1">
|
||||
<Button variant="outline" className="w-full gap-1.5 uppercase font-black border-[#ccff00]/50 text-[#ccff00] hover:bg-[#ccff00]/10 text-xs sm:text-sm">
|
||||
<CreditCard className="h-3.5 w-3.5 flex-shrink-0" />
|
||||
<span className="truncate">Get Your Card</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -145,7 +145,7 @@ export default function LabPage() {
|
|||
</div>
|
||||
|
||||
{/* Service Type Filter */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
<div className="grid grid-cols-3 gap-2 mb-6">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setServiceType('coaching');
|
||||
|
|
@ -153,12 +153,12 @@ export default function LabPage() {
|
|||
}}
|
||||
variant={serviceType === 'coaching' ? 'default' : 'outline'}
|
||||
className={cn(
|
||||
'uppercase font-bold gap-2',
|
||||
'uppercase font-bold gap-1.5 text-xs sm:text-sm',
|
||||
serviceType === 'coaching' ? 'bg-red-500 hover:bg-red-600' : 'border-gray-700'
|
||||
)}
|
||||
>
|
||||
<GraduationCap className="h-4 w-4" />
|
||||
{t('labPage.coachesTab')}
|
||||
<GraduationCap className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">{t('labPage.coachesTab')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
@ -167,12 +167,12 @@ export default function LabPage() {
|
|||
}}
|
||||
variant={serviceType === 'filming' ? 'default' : 'outline'}
|
||||
className={cn(
|
||||
'uppercase font-bold gap-2',
|
||||
'uppercase font-bold gap-1.5 text-xs sm:text-sm',
|
||||
serviceType === 'filming' ? 'bg-orange-500 hover:bg-orange-600' : 'border-gray-700'
|
||||
)}
|
||||
>
|
||||
<Video className="h-4 w-4" />
|
||||
{t('labPage.filmersTab')}
|
||||
<Video className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate">{t('labPage.filmersTab')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
@ -181,12 +181,13 @@ export default function LabPage() {
|
|||
}}
|
||||
variant={serviceType === 'editing' ? 'default' : 'outline'}
|
||||
className={cn(
|
||||
'uppercase font-bold gap-2',
|
||||
'uppercase font-bold gap-1.5 text-xs sm:text-sm',
|
||||
serviceType === 'editing' ? 'bg-purple-600 hover:bg-purple-700' : 'border-gray-700'
|
||||
)}
|
||||
>
|
||||
<Scissors className="h-4 w-4" />
|
||||
Video Editors
|
||||
<Scissors className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate hidden sm:inline">Video Editors</span>
|
||||
<span className="truncate sm:hidden">Editors</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -295,29 +295,29 @@ export default function WalletPage() {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="container px-4 py-8">
|
||||
<div className="container px-4 py-6 md:py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold">Wallet</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
<div className="mb-6 md:mb-8">
|
||||
<h1 className="text-2xl md:text-3xl font-bold">Wallet</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm md:text-base">
|
||||
Manage your Bitcoin and send zaps to skaters and creators
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{user ? (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4 md:space-y-6">
|
||||
{/* Wallet Comparison Guide */}
|
||||
<WalletComparisonGuide />
|
||||
|
||||
<Tabs defaultValue="cashu" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 mb-6">
|
||||
<TabsTrigger value="cashu" className="gap-2">
|
||||
<Nut className="h-4 w-4" />
|
||||
Cashu Wallet
|
||||
<TabsList className="grid w-full grid-cols-2 mb-4 md:mb-6 h-auto">
|
||||
<TabsTrigger value="cashu" className="gap-1.5 py-2.5 text-sm">
|
||||
<Nut className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Cashu Wallet</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="lightning" className="gap-2">
|
||||
<Zap className="h-4 w-4" />
|
||||
Lightning
|
||||
<TabsTrigger value="lightning" className="gap-1.5 py-2.5 text-sm">
|
||||
<Zap className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Lightning</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue