SkateSpots/src/components/DeepLinkHandler.tsx
shakespeare.diy 50914b7b9b fix(amber): call setPackageName before signing so the plugin opens Amber (1.2.9)
Verified against the plugin's Java source: signEvent/nip*Encrypt/Decrypt
IMMEDIATELY reject with "Signer package name not set" when no package is
configured — which is exactly what my earlier "omit packageName" change caused,
and that rejection surfaced as the endless spinner on delete/accept/zap.

With the package set, the plugin tries the content resolver, and when that
returns null (permission not remembered) it falls back to
startActivityForResult(nostrsigner:) — the SAME native call login uses that
reliably opens Amber. Restore native plugin signEvent/nip04/nip44 with
setPackageName + AMBER_PACKAGE, reconstruct signature-only responses, and remove
the unused JS-intent bridge. Bump to 1.2.9.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-09-04 21:44:27 +07:00

78 lines
2.5 KiB
TypeScript

import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
/**
* Forwards OS-level deep-link opens into the React Router navigation stack.
*
* Uses the global window.Capacitor object to avoid a hard npm dependency on
* @capacitor/app (which is installed on the native build machine but not in
* the web/CI build environment).
*
* When the OS launches the app via a URL (Amber NIP-55 callback, custom scheme,
* or universal link), Capacitor fires an `appUrlOpen` event. This component
* listens for it and navigates to the correct in-app route.
*
* Amber deep-link example:
* org.skatespots.app://amber-callback?event=<data>
* → navigates to /amber-callback?event=<data>
*/
// Type for the global Capacitor object injected by the native runtime
interface CapacitorGlobal {
isNativePlatform?: () => boolean;
Plugins?: {
App?: {
addListener: (
event: string,
handler: (data: { url: string }) => void
) => Promise<{ remove: () => void }>;
};
};
}
export function DeepLinkHandler() {
const navigate = useNavigate();
useEffect(() => {
const cap = (window as unknown as { Capacitor?: CapacitorGlobal }).Capacitor;
if (!cap?.isNativePlatform?.()) return;
const AppPlugin = cap?.Plugins?.App;
if (!AppPlugin) return;
let removeListener: (() => void) | undefined;
AppPlugin.addListener('appUrlOpen', (event) => {
try {
const url = new URL(event.url);
// Pre-capture Amber callback event param into sessionStorage so
// AmberCallbackPage can find it after React Router renders the route.
const eventParam = url.searchParams.get('event');
if (eventParam) {
try {
sessionStorage.setItem('amber_callback_event', eventParam);
sessionStorage.setItem('amber_callback_url', event.url);
sessionStorage.setItem('amber_callback_timestamp', Date.now().toString());
} catch { /* ignore storage errors */ }
}
// Extract path + query + fragment from the deep link URL
const path = url.pathname + url.search + url.hash;
if (path && path !== '/') {
navigate(path, { replace: true });
}
} catch {
// Invalid URL — ignore
}
}).then((listener) => {
removeListener = () => listener.remove();
}).catch(() => {
// Plugin not available — ignore
});
return () => {
removeListener?.();
};
}, [navigate]);
return null;
}