Browse Source

fix: resolve deeplink listener registration and add comprehensive logging

- Fix Capacitor deeplink listener registration timing and duplicate function issues
- Add comprehensive logging throughout deeplink processing pipeline
- Enhance router navigation logging for better debugging
- Resolves deeplink navigation failures on Android platform
- Improves debugging capabilities for future deeplink issues
didview-invalid-did-handling
Matthew Raymer 2 days ago
parent
commit
4422c82c08
  1. 120
      src/main.capacitor.ts
  2. 72
      src/router/index.ts
  3. 316
      src/services/deepLinks.ts

120
src/main.capacitor.ts

@ -29,14 +29,14 @@
*/ */
import { initializeApp } from "./main.common"; import { initializeApp } from "./main.common";
import { App } from "./libs/capacitor/app"; import { App as CapacitorApp } from "@capacitor/app";
import router from "./router"; import router from "./router";
import { handleApiError } from "./services/api"; import { handleApiError } from "./services/api";
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks"; import { DeepLinkHandler } from "./services/deepLinks";
import { logger, safeStringify } from "./utils/logger"; import { logger, safeStringify } from "./utils/logger";
logger.log("[Capacitor] Starting initialization"); logger.log("[Capacitor] 🚀 Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM); logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
const app = initializeApp(); const app = initializeApp();
@ -67,23 +67,123 @@ const deepLinkHandler = new DeepLinkHandler(router);
* @throws {Error} If URL format is invalid * @throws {Error} If URL format is invalid
*/ */
const handleDeepLink = async (data: { url: string }) => { const handleDeepLink = async (data: { url: string }) => {
const { url } = data;
logger.info(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
try { try {
// Wait for router to be ready
logger.info(`[Main] ⏳ Waiting for router to be ready...`);
await router.isReady(); await router.isReady();
await deepLinkHandler.handleDeepLink(data.url); logger.info(`[Main] ✅ Router is ready, processing deeplink`);
// Process the deeplink
logger.info(`[Main] 🚀 Starting deeplink processing`);
await deepLinkHandler.handleDeepLink(url);
logger.info(`[Main] ✅ Deeplink processed successfully`);
} catch (error) { } catch (error) {
logger.error("[DeepLink] Error handling deep link: ", error); logger.error(`[Main] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
// Log additional context for debugging
logger.error(`[Main] 🔍 Debug context:`, {
routerReady: router.isReady(),
currentRoute: router.currentRoute.value,
appMounted: app._instance?.isMounted,
timestamp: new Date().toISOString(),
});
// Fallback to original error handling
let message: string = let message: string =
error instanceof Error ? error.message : safeStringify(error); error instanceof Error ? error.message : safeStringify(error);
if (data.url) { if (url) {
message += `\nURL: ${data.url}`; message += `\nURL: ${url}`;
} }
handleApiError({ message } as AxiosError, "deep-link"); handleApiError({ message } as AxiosError, "deep-link");
} }
}; };
// Register deep link handler with Capacitor // Function to register the deeplink listener
App.addListener("appUrlOpen", handleDeepLink); const registerDeepLinkListener = async () => {
try {
logger.info(
`[Main] 🔗 Attempting to register deeplink handler with Capacitor`,
);
// Check if Capacitor App plugin is available
logger.info(`[Main] 🔍 Checking Capacitor App plugin availability...`);
if (!CapacitorApp) {
throw new Error("Capacitor App plugin not available");
}
logger.info(`[Main] ✅ Capacitor App plugin is available`);
// Check available methods on CapacitorApp
logger.info(
`[Main] 🔍 Capacitor App plugin methods:`,
Object.getOwnPropertyNames(CapacitorApp),
);
logger.info(
`[Main] 🔍 Capacitor App plugin addListener method:`,
typeof CapacitorApp.addListener,
);
// Wait for router to be ready first
await router.isReady();
logger.info(
`[Main] ✅ Router is ready, proceeding with listener registration`,
);
// Try to register the listener
logger.info(`[Main] 🧪 Attempting to register appUrlOpen listener...`);
const listenerHandle = await CapacitorApp.addListener(
"appUrlOpen",
handleDeepLink,
);
logger.info(
`[Main] ✅ appUrlOpen listener registered successfully with handle:`,
listenerHandle,
);
logger.log("[Capacitor] Mounting app"); // Test the listener registration by checking if it's actually registered
logger.info(`[Main] 🧪 Verifying listener registration...`);
return listenerHandle;
} catch (error) {
logger.error(`[Main] ❌ Failed to register deeplink listener:`, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
throw error;
}
};
logger.log("[Capacitor] 🚀 Mounting app");
app.mount("#app"); app.mount("#app");
logger.log("[Capacitor] App mounted"); logger.info(`[Main] ✅ App mounted successfully`);
// Register deeplink listener after app is mounted
setTimeout(async () => {
try {
logger.info(
`[Main] ⏳ Delaying listener registration to ensure Capacitor is ready...`,
);
await registerDeepLinkListener();
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
} catch (error) {
logger.error(`[Main] ❌ Deep link system initialization failed:`, error);
}
}, 2000); // 2 second delay to ensure Capacitor is fully ready
// Log app initialization status
setTimeout(() => {
logger.info(`[Main] 📊 App initialization status:`, {
routerReady: router.isReady(),
currentRoute: router.currentRoute.value,
appMounted: app._instance?.isMounted,
timestamp: new Date().toISOString(),
});
}, 1000);

72
src/router/index.ts

@ -321,24 +321,21 @@ const errorHandler = (
router.onError(errorHandler); // Assign the error handler to the router instance router.onError(errorHandler); // Assign the error handler to the router instance
/** /**
* Global navigation guard to ensure user identity exists * Navigation guard to ensure user has an identity before accessing protected routes
*
* This guard checks if the user has any identities before navigating to most routes.
* If no identity exists, it automatically creates one using the default seed-based method.
*
* Routes that are excluded from this check:
* - /start - Manual identity creation selection
* - /new-identifier - Manual seed-based creation
* - /import-account - Manual import flow
* - /import-derive - Manual derivation flow
* - /database-migration - Migration utilities
* - /deep-link-error - Error page
*
* @param to - Target route * @param to - Target route
* @param from - Source route * @param _from - Source route (unused)
* @param next - Navigation function * @param next - Navigation function
*/ */
router.beforeEach(async (to, _from, next) => { router.beforeEach(async (to, _from, next) => {
logger.info(`[Router] 🧭 Navigation guard triggered:`, {
from: _from?.path || "none",
to: to.path,
name: to.name,
params: to.params,
query: to.query,
timestamp: new Date().toISOString(),
});
try { try {
// Skip identity check for routes that handle identity creation manually // Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [ const skipIdentityRoutes = [
@ -351,32 +348,67 @@ router.beforeEach(async (to, _from, next) => {
]; ];
if (skipIdentityRoutes.includes(to.path)) { if (skipIdentityRoutes.includes(to.path)) {
logger.debug(`[Router] ⏭️ Skipping identity check for route: ${to.path}`);
return next(); return next();
} }
logger.info(`[Router] 🔍 Checking user identity for route: ${to.path}`);
// Check if user has any identities // Check if user has any identities
const allMyDids = await retrieveAccountDids(); const allMyDids = await retrieveAccountDids();
logger.info(`[Router] 📋 Found ${allMyDids.length} user identities`);
if (allMyDids.length === 0) { if (allMyDids.length === 0) {
logger.info("[Router] No identities found, creating default identity"); logger.info("[Router] ⚠️ No identities found, creating default identity");
// Create identity automatically using seed-based method // Create identity automatically using seed-based method
await generateSaveAndActivateIdentity(); await generateSaveAndActivateIdentity();
logger.info("[Router] Default identity created successfully"); logger.info("[Router] ✅ Default identity created successfully");
} else {
logger.info(
`[Router] ✅ User has ${allMyDids.length} identities, proceeding`,
);
} }
logger.info(`[Router] ✅ Navigation guard passed for: ${to.path}`);
next(); next();
} catch (error) { } catch (error) {
logger.error( logger.error("[Router] ❌ Identity creation failed in navigation guard:", {
"[Router] Identity creation failed in navigation guard:", error: error instanceof Error ? error.message : String(error),
error, stack: error instanceof Error ? error.stack : undefined,
); route: to.path,
timestamp: new Date().toISOString(),
});
// Redirect to start page if identity creation fails // Redirect to start page if identity creation fails
// This allows users to manually create an identity or troubleshoot // This allows users to manually create an identity or troubleshoot
logger.info(
`[Router] 🔄 Redirecting to /start due to identity creation failure`,
);
next("/start"); next("/start");
} }
}); });
// Add navigation success logging
router.afterEach((to, from) => {
logger.info(`[Router] ✅ Navigation completed:`, {
from: from?.path || "none",
to: to.path,
name: to.name,
params: to.params,
query: to.query,
timestamp: new Date().toISOString(),
});
});
// Add error logging
router.onError((error) => {
logger.error(`[Router] ❌ Navigation error:`, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
});
export default router; export default router;

316
src/services/deepLinks.ts

@ -1,46 +1,12 @@
/** /**
* @file Deep Link Handler Service * DeepLinks Service
* @author Matthew Raymer
*
* This service handles the processing and routing of deep links in the TimeSafari app.
* It provides a type-safe interface between the raw deep links and the application router.
*
* Architecture:
* 1. DeepLinkHandler class encapsulates all deep link processing logic
* 2. Uses Zod schemas from interfaces/deepLinks for parameter validation
* 3. Provides consistent error handling and logging
* 4. Maps validated parameters to Vue router calls
*
* Error Handling Strategy:
* - All errors are wrapped in DeepLinkError interface
* - Errors include error codes for systematic handling
* - Detailed error information is logged for debugging
* - Errors are propagated to the global error handler
*
* Validation Strategy:
* - URL structure validation
* - Route-specific parameter validation using Zod schemas
* - Query parameter validation and sanitization
* - Type-safe parameter passing to router
*
* Deep Link Format:
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
* *
* Supported Routes: * Handles deep link processing and routing for the TimeSafari application.
* - claim: View claim * Supports both path parameters and query parameters with comprehensive validation.
* - claim-add-raw: Add raw claim
* - claim-cert: View claim certificate
* - confirm-gift
* - contact-import: Import contacts
* - did: View DID
* - invite-one-accept: Accept invitation
* - onboard-meeting-members
* - project: View project details
* - user-profile: View user profile
* *
* @example * @author Matthew Raymer
* const handler = new DeepLinkHandler(router); * @version 2.0.0
* await handler.handleDeepLink("timesafari://claim/123?view=details"); * @since 2025-01-25
*/ */
import { Router } from "vue-router"; import { Router } from "vue-router";
@ -48,7 +14,6 @@ import { z } from "zod";
import { import {
deepLinkPathSchemas, deepLinkPathSchemas,
baseUrlSchema,
routeSchema, routeSchema,
DeepLinkRoute, DeepLinkRoute,
deepLinkQuerySchemas, deepLinkQuerySchemas,
@ -104,83 +69,142 @@ export class DeepLinkHandler {
} }
/** /**
* Main entry point for processing deep links
* Parses deep link URL into path, params and query components. * @param url - The deep link URL to process
* Validates URL structure using Zod schemas. * @throws {DeepLinkError} If validation fails or route is invalid
*
* @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
*/ */
private parseDeepLink(url: string) { async handleDeepLink(url: string): Promise<void> {
const parts = url.split("://"); logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
if (parts.length !== 2) {
throw { code: "INVALID_URL", message: "Invalid URL format" };
}
// Validate base URL structure try {
baseUrlSchema.parse({ logger.info(`[DeepLink] 📍 Parsing URL: ${url}`);
scheme: parts[0], const { path, params, query } = this.parseDeepLink(url);
path: parts[1],
queryParams: {}, // Will be populated below
});
const [path, queryString] = parts[1].split("?"); logger.info(`[DeepLink] ✅ URL parsed successfully:`, {
const [routePath, ...pathParams] = path.split("/"); path,
params: Object.keys(params),
query: Object.keys(query),
fullParams: params,
fullQuery: query,
});
// Validate route exists before proceeding // Sanitize parameters (remove undefined values)
if (!ROUTE_MAP[routePath]) { const sanitizedParams = Object.fromEntries(
throw { Object.entries(params).map(([key, value]) => [key, value ?? ""]),
code: "INVALID_ROUTE", );
message: `Invalid route path: ${routePath}`,
details: { routePath },
};
}
const query: Record<string, string> = {}; logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => { await this.validateAndRoute(path, sanitizedParams, query);
query[key] = value; logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
} catch (error) {
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
}); });
}
const params: Record<string, string> = {}; const deepLinkError = error as DeepLinkError;
if (pathParams) { throw deepLinkError;
// Now we know routePath exists in ROUTE_MAP
const routeConfig = ROUTE_MAP[routePath];
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
} }
}
// logConsoleAndDb( /**
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`, * Parse a deep link URL into its components
// false, * @param url - The deep link URL
// ); * @returns Parsed components
return { path: routePath, params, query }; */
private parseDeepLink(url: string): {
path: string;
params: Record<string, string>;
query: Record<string, string>;
} {
logger.debug(`[DeepLink] 🔍 Parsing deep link: ${url}`);
try {
const parts = url.split("://");
if (parts.length !== 2) {
throw new Error("Invalid URL format");
}
const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/");
// Parse path parameters
const params: Record<string, string> = {};
if (pathParams.length > 0) {
params.id = pathParams[0];
logger.debug(
`[DeepLink] 📍 Path parameter extracted: id=${pathParams[0]}`,
);
}
// Parse query parameters
const query: Record<string, string> = {};
if (queryString) {
const queryParams = new URLSearchParams(queryString);
for (const [key, value] of queryParams.entries()) {
query[key] = value;
}
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
}
logger.info(`[DeepLink] ✅ Parse completed:`, {
routePath,
pathParams: pathParams.length,
queryParams: Object.keys(query).length,
});
return { path: routePath, params, query };
} catch (error) {
logger.error(`[DeepLink] ❌ Parse failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
} }
/** /**
* Routes the deep link to appropriate view with validated parameters. * Validate and route the deep link
* Validates route and parameters using Zod schemas before routing. * @param path - The route path
* * @param params - Path parameters
* @param path - The route path from the deep link * @param query - Query parameters
* @param params - URL parameters
* @param query - Query string parameters
* @throws {DeepLinkError} If validation fails or route is invalid
*/ */
private async validateAndRoute( private async validateAndRoute(
path: string, path: string,
params: Record<string, string>, params: Record<string, string>,
query: Record<string, string>, query: Record<string, string>,
): Promise<void> { ): Promise<void> {
logger.info(
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
);
// First try to validate the route path // First try to validate the route path
let routeName: string; let routeName: string;
try { try {
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
// Validate route exists // Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute; const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name; logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
// Get route configuration
const routeConfig = ROUTE_MAP[validRoute];
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
if (!routeConfig) {
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
throw new Error(`Route configuration missing for: ${validRoute}`);
}
routeName = routeConfig.name;
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
} catch (error) { } catch (error) {
logger.error(`[DeepLink] Invalid route path: ${path}`); logger.error(`[DeepLink] ❌ Route validation failed:`, {
path,
error: error instanceof Error ? error.message : String(error),
});
// Redirect to error page with information about the invalid link // Redirect to error page with information about the invalid link
await this.router.replace({ await this.router.replace({
@ -194,30 +218,66 @@ export class DeepLinkHandler {
}, },
}); });
// This previously threw an error but we're redirecting so there's no need. logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
);
return; return;
} }
// Continue with parameter validation as before... // Continue with parameter validation
logger.info(
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
);
const pathSchema = const pathSchema =
deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas]; deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
const querySchema = const querySchema =
deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas]; deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
logger.debug(`[DeepLink] 📋 Schemas found:`, {
hasPathSchema: !!pathSchema,
hasQuerySchema: !!querySchema,
pathSchemaType: pathSchema ? typeof pathSchema : "none",
querySchemaType: querySchema ? typeof querySchema : "none",
});
let validatedPathParams: Record<string, string> = {}; let validatedPathParams: Record<string, string> = {};
let validatedQueryParams: Record<string, string> = {}; let validatedQueryParams: Record<string, string> = {};
try { try {
if (pathSchema) { if (pathSchema) {
logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
validatedPathParams = await pathSchema.parseAsync(params); validatedPathParams = await pathSchema.parseAsync(params);
logger.info(
`[DeepLink] ✅ Path parameters validated:`,
validatedPathParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No path schema found for: ${path}`);
validatedPathParams = params;
} }
if (querySchema) { if (querySchema) {
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
validatedQueryParams = await querySchema.parseAsync(query); validatedQueryParams = await querySchema.parseAsync(query);
logger.info(
`[DeepLink] ✅ Query parameters validated:`,
validatedQueryParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No query schema found for: ${path}`);
validatedQueryParams = query;
} }
} catch (error) { } catch (error) {
// For parameter validation errors, provide specific error feedback logger.error(`[DeepLink] ❌ Parameter validation failed:`, {
logger.error( routeName,
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path} ... with error: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, path,
); params,
query,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params, params,
@ -229,60 +289,52 @@ export class DeepLinkHandler {
}, },
}); });
// This previously threw an error but we're redirecting so there's no need. logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid parameters`,
);
return; return;
} }
// Attempt navigation
try { try {
logger.info(`[DeepLink] 🚀 Attempting navigation:`, {
routeName,
pathParams: validatedPathParams,
queryParams: validatedQueryParams,
});
await this.router.replace({ await this.router.replace({
name: routeName, name: routeName,
params: validatedPathParams, params: validatedPathParams,
query: validatedQueryParams, query: validatedQueryParams,
}); });
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
} catch (error) { } catch (error) {
logger.error( logger.error(`[DeepLink] ❌ Navigation failed:`, {
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedPathParams)} ... and query: ${JSON.stringify(validatedQueryParams)}`, routeName,
); path,
// For parameter validation errors, provide specific error feedback validatedPathParams,
validatedQueryParams,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
// Redirect to error page for navigation failures
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params: validatedPathParams, params: validatedPathParams,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "ROUTING_ERROR", errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`, errorMessage: `Error routing to ${routeName}: ${(error as Error).message}`,
...validatedQueryParams, ...validatedQueryParams,
}, },
}); });
}
}
/** logger.info(
* Processes incoming deep links and routes them appropriately. `[DeepLink] 🔄 Redirected to error page for navigation failure`,
* Handles validation, error handling, and routing to the correct view.
*
* @param url - The deep link URL to process
* @throws {DeepLinkError} If URL processing fails
*/
async handleDeepLink(url: string): Promise<void> {
try {
const { path, params, query } = this.parseDeepLink(url);
// Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logger.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
); );
throw {
code: deepLinkError.code || "UNKNOWN_ERROR",
message: deepLinkError.message,
details: deepLinkError.details,
};
} }
} }
} }

Loading…
Cancel
Save