fix: switch to HashRouter for Capacitor Android compatibility
The primary cause of the white screen in the APK was BrowserRouter. Capacitor serves files via https://localhost/ and the HTML5 History API routing (BrowserRouter) fails to resolve routes in this context, resulting in a blank white page. Changes: - Switch BrowserRouter → HashRouter in AppRouter.tsx - Add DeepLinkHandler component (handles appUrlOpen for Amber callbacks and other deep links in native context) - Fix Amber callback URL builder to use org.skatespots.app:// custom scheme when running in Capacitor (so Android fires an intent back into the app instead of navigating a browser) - Fix getSearchParams() in amber.ts to extract query params from hash fragment (HashRouter URLs look like /#/amber-callback?event=XXX) - Fix captureAmberCallback in main.tsx to check hash fragment too - Remove debug error overlay (no longer needed) Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
parent
8c25110f44
commit
c3ef0247fc
4 changed files with 133 additions and 33 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { lazy, Suspense } from "react";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { HashRouter, Route, Routes } from "react-router-dom";
|
||||
import { ScrollToTop } from "./components/ScrollToTop";
|
||||
import { DeepLinkHandler } from "./components/DeepLinkHandler";
|
||||
|
||||
// Eagerly load only the most-visited pages
|
||||
import Index from "./pages/Index";
|
||||
|
|
@ -61,7 +62,8 @@ function PageLoader() {
|
|||
|
||||
export function AppRouter() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<HashRouter>
|
||||
<DeepLinkHandler />
|
||||
<ScrollToTop />
|
||||
<Suspense fallback={<PageLoader />}>
|
||||
<Routes>
|
||||
|
|
@ -122,7 +124,7 @@ export function AppRouter() {
|
|||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
export default AppRouter;
|
||||
|
|
|
|||
62
src/components/DeepLinkHandler.tsx
Normal file
62
src/components/DeepLinkHandler.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Forwards OS-level deep-link opens into the React Router navigation stack.
|
||||
*
|
||||
* 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=<data>
|
||||
* → navigates to /amber-callback?event=<data>
|
||||
*/
|
||||
export function DeepLinkHandler() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// Only run in Capacitor native context
|
||||
const cap = (window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor;
|
||||
if (!cap?.isNativePlatform?.()) return;
|
||||
|
||||
let removeListener: (() => void) | undefined;
|
||||
|
||||
async function setup() {
|
||||
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();
|
||||
} catch {
|
||||
// @capacitor/app not available (web build without Capacitor installed)
|
||||
}
|
||||
}
|
||||
|
||||
setup();
|
||||
|
||||
return () => {
|
||||
removeListener?.();
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -178,12 +178,34 @@ export function clearAmberLogin(): void {
|
|||
localStorage.removeItem(AMBER_LOGIN_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract search params from both the real query string AND the hash fragment.
|
||||
*
|
||||
* With HashRouter the URL looks like:
|
||||
* https://localhost/#/amber-callback?event=XXXX
|
||||
* In that case `window.location.search` is empty but the params are embedded
|
||||
* inside `window.location.hash` after the path segment.
|
||||
*/
|
||||
function getSearchParams(): URLSearchParams {
|
||||
// Normal query string (PWA / browser)
|
||||
if (window.location.search) {
|
||||
return new URLSearchParams(window.location.search);
|
||||
}
|
||||
// HashRouter: extract the query string portion from the hash
|
||||
const hash = window.location.hash; // e.g. "#/amber-callback?event=XXX"
|
||||
const qIndex = hash.indexOf('?');
|
||||
if (qIndex !== -1) {
|
||||
return new URLSearchParams(hash.slice(qIndex));
|
||||
}
|
||||
return new URLSearchParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current page was opened as an Amber callback.
|
||||
* Also checks pre-captured data from main.tsx for PWA resilience.
|
||||
*/
|
||||
export function isAmberCallback(): boolean {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const params = getSearchParams();
|
||||
if (params.has('event')) return true;
|
||||
|
||||
// Check pre-captured data (PWAs might lose URL params during hydration)
|
||||
|
|
@ -201,7 +223,7 @@ export function isAmberCallback(): boolean {
|
|||
* Falls back to pre-captured data from main.tsx.
|
||||
*/
|
||||
export function getAmberCallbackResult(): string | null {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const params = getSearchParams();
|
||||
const fromUrl = params.get('event');
|
||||
|
||||
if (fromUrl) {
|
||||
|
|
@ -225,10 +247,17 @@ export function getAmberCallbackResult(): string | null {
|
|||
|
||||
/**
|
||||
* Clean the URL after processing Amber callback.
|
||||
* With HashRouter we replace the hash to remove the query params.
|
||||
*/
|
||||
export function cleanAmberCallbackUrl(): void {
|
||||
const cleanUrl = window.location.origin + window.location.pathname;
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
if (window.location.hash.includes('?')) {
|
||||
// HashRouter: strip query from hash fragment
|
||||
const hashPath = window.location.hash.split('?')[0];
|
||||
window.history.replaceState({}, '', window.location.pathname + hashPath);
|
||||
} else {
|
||||
const cleanUrl = window.location.origin + window.location.pathname;
|
||||
window.history.replaceState({}, '', cleanUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -240,9 +269,22 @@ function generateOperationId(): string {
|
|||
|
||||
/**
|
||||
* Build the dedicated Amber callback URL.
|
||||
* Using a fixed /amber-callback path so Amber always returns to the same place.
|
||||
*
|
||||
* In the Capacitor APK we must use the app's custom URI scheme so Android fires
|
||||
* an intent back into the app (not a browser navigation).
|
||||
* Web/PWA builds use the normal https origin.
|
||||
*
|
||||
* The app ID is `org.skatespots.app` — Capacitor registers this as a URI scheme
|
||||
* automatically via the `appId` in capacitor.config.ts.
|
||||
*/
|
||||
function buildCallbackUrl(): string {
|
||||
const isCapacitor = !!(window as unknown as { Capacitor?: { isNativePlatform?: () => boolean } }).Capacitor?.isNativePlatform?.();
|
||||
if (isCapacitor) {
|
||||
// Use the app's custom scheme so Amber fires the intent back into the APK.
|
||||
// DeepLinkHandler in AppRouter.tsx catches appUrlOpen and navigates to the
|
||||
// correct route inside the HashRouter.
|
||||
return 'org.skatespots.app://amber-callback?event=';
|
||||
}
|
||||
return `${window.location.origin}/amber-callback?event=`;
|
||||
}
|
||||
|
||||
|
|
|
|||
44
src/main.tsx
44
src/main.tsx
|
|
@ -29,46 +29,40 @@ import '@fontsource/barlow-condensed/700.css';
|
|||
|
||||
/**
|
||||
* AMBER CALLBACK PRE-CHECK
|
||||
*
|
||||
*
|
||||
* PWAs can sometimes lose URL parameters during React hydration.
|
||||
* This runs BEFORE React renders to capture Amber callback data.
|
||||
* We store it in sessionStorage so the AmberCallbackHandler can retrieve it.
|
||||
*
|
||||
* Handles both:
|
||||
* - Normal query string: ?event=XXX (PWA / browser)
|
||||
* - HashRouter fragment: #/amber-callback?event=XXX (Capacitor APK)
|
||||
*/
|
||||
(function captureAmberCallback() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const event = params.get('event');
|
||||
|
||||
if (event) {
|
||||
// Try normal query string first
|
||||
let eventParam = new URLSearchParams(window.location.search).get('event');
|
||||
|
||||
// Try hash fragment (HashRouter + Capacitor deep link)
|
||||
if (!eventParam) {
|
||||
const hash = window.location.hash;
|
||||
const qIdx = hash.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
eventParam = new URLSearchParams(hash.slice(qIdx)).get('event');
|
||||
}
|
||||
}
|
||||
|
||||
if (eventParam) {
|
||||
console.log('[Amber Pre-Check] Detected Amber callback in URL');
|
||||
console.log('[Amber Pre-Check] Full URL:', window.location.href);
|
||||
console.log('[Amber Pre-Check] Event param:', event);
|
||||
|
||||
// Store the callback data immediately before React can interfere
|
||||
try {
|
||||
sessionStorage.setItem('amber_callback_event', event);
|
||||
sessionStorage.setItem('amber_callback_event', eventParam);
|
||||
sessionStorage.setItem('amber_callback_url', window.location.href);
|
||||
sessionStorage.setItem('amber_callback_timestamp', Date.now().toString());
|
||||
console.log('[Amber Pre-Check] Stored callback data in sessionStorage');
|
||||
} catch (e) {
|
||||
console.error('[Amber Pre-Check] Failed to store callback:', e);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// Global error trap — shows error on screen in the APK so we can debug
|
||||
// without needing USB/adb. Remove once white screen is fixed.
|
||||
window.onerror = (msg, src, line, col, err) => {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:#000;color:#ff0040;padding:16px;font:14px monospace;z-index:99999;overflow:auto;white-space:pre-wrap;word-break:break-all;';
|
||||
div.textContent = `ERROR:\n${msg}\n\n${src}:${line}:${col}\n\n${err?.stack || ''}`;
|
||||
document.body.appendChild(div);
|
||||
};
|
||||
window.onunhandledrejection = (e) => {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:#000;color:#ff6600;padding:16px;font:14px monospace;z-index:99999;overflow:auto;white-space:pre-wrap;word-break:break-all;';
|
||||
div.textContent = `UNHANDLED REJECTION:\n${e.reason?.stack || e.reason || 'unknown'}`;
|
||||
document.body.appendChild(div);
|
||||
};
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<ErrorBoundary>
|
||||
|
|
|
|||
Loading…
Reference in a new issue