From be70ff490fc35ad57cb1954bf50ac997a1d47e1c Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 18 Jun 2026 22:01:40 +0700 Subject: [PATCH] fix: remove @capacitor/app dynamic import from DeepLinkHandler Use window.Capacitor.Plugins.App directly instead of dynamic import() so the build doesn't fail when @capacitor/app isn't in node_modules Co-authored-by: shakespeare.diy --- src/components/DeepLinkHandler.tsx | 64 ++++++++++++++++-------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/components/DeepLinkHandler.tsx b/src/components/DeepLinkHandler.tsx index f97c751..a0f4f09 100644 --- a/src/components/DeepLinkHandler.tsx +++ b/src/components/DeepLinkHandler.tsx @@ -4,54 +4,60 @@ 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. * - * Uses HashRouter compatible path extraction — strips the origin/scheme so - * only the pathname+search+hash is passed to navigate(). - * - * Must be rendered inside a Router (so `useNavigate` works). - * Safe to render unconditionally — on web it is a no-op. - * * Amber deep-link example: * org.skatespots.app://amber-callback?event= * → navigates to /amber-callback?event= */ + +// 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(() => { - // Only run in Capacitor native context - const cap = (window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor; + 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; - async function setup() { + AppPlugin.addListener('appUrlOpen', (event) => { try { - const { App } = await import('@capacitor/app'); - - const listener = await App.addListener('appUrlOpen', (event) => { - try { - const url = new URL(event.url); - // 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 - } - }); - - removeListener = () => listener.remove(); + const url = new URL(event.url); + // 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 { - // @capacitor/app not available (web build without Capacitor installed) + // Invalid URL — ignore } - } - - setup(); + }).then((listener) => { + removeListener = () => listener.remove(); + }).catch(() => { + // Plugin not available — ignore + }); return () => { removeListener?.();