import { createApp } from "vue";
import { createPinia } from "pinia";
import { App as CapacitorApp } from "@capacitor/app";
import App from "./App.vue";
import { router } from "../router";
import { useGlobalErrorHandler } from "../composables/useGlobalErrorHandler";
import { useAuthStore } from "../stores/auth.js";
import { isDebugEnabled, safeStringify } from "../utils/logger";
import { initServerUrl, isNativeApp } from "../api/server-config";
import { initAccessToken, setAccessToken } from "../api/auth";
import "@phosphor-icons/web/regular";
import "@phosphor-icons/web/fill";
import "../styles/main.css";
const LOG_PREFIX = "[vue:pinia]";
// Pinia store action logger plugin
const storeLogger = (context) => {
if (!isDebugEnabled()) return;
const { store, options } = context;
const storeName = options.id;
// Wrap actions
const actionNames = Object.keys(options.actions || {});
for (const actionName of actionNames) {
const originalAction = store[actionName];
store[actionName] = async function (...args) {
console.debug(LOG_PREFIX, `${storeName}.${actionName}(${args.map(a => safeStringify(a)).join(", ")})`);
const start = performance.now();
try {
const result = await originalAction.apply(this, args);
console.debug(LOG_PREFIX, `${storeName}.${actionName} completed in ${(performance.now() - start).toFixed(1)}ms`);
return result;
} catch (err) {
console.error(LOG_PREFIX, `${storeName}.${actionName} failed: ${err?.message || err}`);
throw err;
}
};
}
};
function handleDeepLink(urlString) {
try {
const url = new URL(urlString);
if (url.host === "auth" && url.pathname === "/callback") {
const token = url.searchParams.get("token");
const expiresIn = url.searchParams.get("expires_in");
if (token) {
setAccessToken(token, expiresIn ? parseInt(expiresIn, 10) : null);
window.location.reload();
}
}
} catch {
// ignore malformed URLs
}
}
async function bootstrap() {
// 1. Load server URL and access token (works on both web and native)
await initServerUrl();
await initAccessToken();
// 2. Configure deep links for native app (no-op on web)
//
// Deep link flow (Android):
// 1. User clicks Sign In → redirectToOAuth(getOAuthReturnTo())
// 2. Capacitor BridgeWebViewClient sees external host → launches
// Intent.ACTION_VIEW (full Chrome), NOT Custom Tabs.
// 3. OAuth completes on gnexus-auth → server redirects to /auth/callback
// 4. AuthController::callback() redirects to /auth/mobile-bridge
// (returnTo preserved from session BEFORE state is wiped by handleCallback)
// 5. Bridge page executes: window.location.href =
// "intent://auth/callback?token=...#Intent;scheme=shserv;package=com.gnexus.shserv;end"
// 6. Android resolves intent:// → relaunches app with shserv://auth/callback?token=...
// 7. Capacitor fires appUrlOpen (warm) or getLaunchUrl() (cold) → handleDeepLink()
// 8. handleDeepLink() extracts token, calls setAccessToken() (Preferences)
// 9. window.location.reload() → bootstrap() restarts, initAccessToken() loads token
// 10. authStore.init() sees valid token → user is authenticated
if (isNativeApp()) {
// Handle cold-start deep link (OAuth bridge page redirecting back to app)
try {
const launchUrl = await CapacitorApp.getLaunchUrl();
if (launchUrl?.url) {
handleDeepLink(launchUrl.url);
return; // window.location.reload() above will restart bootstrap
}
} catch {
// ignore
}
// Handle warm-start deep link when app is already running
CapacitorApp.addListener("appUrlOpen", (data) => {
handleDeepLink(data.url);
});
}
// 3. Create app
const app = createApp(App);
useGlobalErrorHandler(app);
const pinia = createPinia();
pinia.use(storeLogger);
app.use(pinia).use(router);
// 4. Initialize auth before mounting so router guards have valid state
const authStore = useAuthStore();
await authStore.init();
// 5. Mount
app.mount("#app");
}
bootstrap();