Newer
Older
smart-home-server / webclient / src / app / main.js
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();