From 4422c82c08328d229bdb9d7b49d5550c4a9999f4 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 20 Aug 2025 06:41:37 +0000 Subject: [PATCH] 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 --- src/main.capacitor.ts | 120 +++++++++++++-- src/router/index.ts | 72 ++++++--- src/services/deepLinks.ts | 316 ++++++++++++++++++++++---------------- 3 files changed, 346 insertions(+), 162 deletions(-) diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index 191d356e..adc0d8c5 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -29,14 +29,14 @@ */ import { initializeApp } from "./main.common"; -import { App } from "./libs/capacitor/app"; +import { App as CapacitorApp } from "@capacitor/app"; import router from "./router"; import { handleApiError } from "./services/api"; import { AxiosError } from "axios"; import { DeepLinkHandler } from "./services/deepLinks"; import { logger, safeStringify } from "./utils/logger"; -logger.log("[Capacitor] Starting initialization"); +logger.log("[Capacitor] ๐Ÿš€ Starting initialization"); logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM); const app = initializeApp(); @@ -67,23 +67,123 @@ const deepLinkHandler = new DeepLinkHandler(router); * @throws {Error} If URL format is invalid */ const handleDeepLink = async (data: { url: string }) => { + const { url } = data; + logger.info(`[Main] ๐ŸŒ Deeplink received from Capacitor: ${url}`); + try { + // Wait for router to be ready + logger.info(`[Main] โณ Waiting for router to be ready...`); 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) { - 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 = error instanceof Error ? error.message : safeStringify(error); - if (data.url) { - message += `\nURL: ${data.url}`; + if (url) { + message += `\nURL: ${url}`; } handleApiError({ message } as AxiosError, "deep-link"); } }; -// Register deep link handler with Capacitor -App.addListener("appUrlOpen", handleDeepLink); +// Function to register the deeplink listener +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"); -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); diff --git a/src/router/index.ts b/src/router/index.ts index 043d3d0c..f80d7a48 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -321,24 +321,21 @@ const errorHandler = ( router.onError(errorHandler); // Assign the error handler to the router instance /** - * Global navigation guard to ensure user identity exists - * - * 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 - * + * Navigation guard to ensure user has an identity before accessing protected routes * @param to - Target route - * @param from - Source route + * @param _from - Source route (unused) * @param next - Navigation function */ 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 { // Skip identity check for routes that handle identity creation manually const skipIdentityRoutes = [ @@ -351,32 +348,67 @@ router.beforeEach(async (to, _from, next) => { ]; if (skipIdentityRoutes.includes(to.path)) { + logger.debug(`[Router] โญ๏ธ Skipping identity check for route: ${to.path}`); return next(); } + logger.info(`[Router] ๐Ÿ” Checking user identity for route: ${to.path}`); + // Check if user has any identities const allMyDids = await retrieveAccountDids(); + logger.info(`[Router] ๐Ÿ“‹ Found ${allMyDids.length} user identities`); 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 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(); } catch (error) { - logger.error( - "[Router] Identity creation failed in navigation guard:", - error, - ); + logger.error("[Router] โŒ Identity creation failed in navigation guard:", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + route: to.path, + timestamp: new Date().toISOString(), + }); // Redirect to start page if identity creation fails // This allows users to manually create an identity or troubleshoot + logger.info( + `[Router] ๐Ÿ”„ Redirecting to /start due to identity creation failure`, + ); 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; diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index ee6095bb..750848a3 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -1,46 +1,12 @@ /** - * @file Deep Link Handler 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://[/][?queryParam1=value1&queryParam2=value2] + * DeepLinks Service * - * Supported Routes: - * - claim: View claim - * - 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 + * Handles deep link processing and routing for the TimeSafari application. + * Supports both path parameters and query parameters with comprehensive validation. * - * @example - * const handler = new DeepLinkHandler(router); - * await handler.handleDeepLink("timesafari://claim/123?view=details"); + * @author Matthew Raymer + * @version 2.0.0 + * @since 2025-01-25 */ import { Router } from "vue-router"; @@ -48,7 +14,6 @@ import { z } from "zod"; import { deepLinkPathSchemas, - baseUrlSchema, routeSchema, DeepLinkRoute, deepLinkQuerySchemas, @@ -104,83 +69,142 @@ export class DeepLinkHandler { } /** - - * Parses deep link URL into path, params and query components. - * Validates URL structure using Zod schemas. - * - * @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}) + * Main entry point for processing deep links + * @param url - The deep link URL to process + * @throws {DeepLinkError} If validation fails or route is invalid */ - private parseDeepLink(url: string) { - const parts = url.split("://"); - if (parts.length !== 2) { - throw { code: "INVALID_URL", message: "Invalid URL format" }; - } + async handleDeepLink(url: string): Promise { + logger.info(`[DeepLink] ๐Ÿš€ Starting deeplink processing for URL: ${url}`); - // Validate base URL structure - baseUrlSchema.parse({ - scheme: parts[0], - path: parts[1], - queryParams: {}, // Will be populated below - }); + try { + logger.info(`[DeepLink] ๐Ÿ“ Parsing URL: ${url}`); + const { path, params, query } = this.parseDeepLink(url); - const [path, queryString] = parts[1].split("?"); - const [routePath, ...pathParams] = path.split("/"); + logger.info(`[DeepLink] โœ… URL parsed successfully:`, { + path, + params: Object.keys(params), + query: Object.keys(query), + fullParams: params, + fullQuery: query, + }); - // Validate route exists before proceeding - if (!ROUTE_MAP[routePath]) { - throw { - code: "INVALID_ROUTE", - message: `Invalid route path: ${routePath}`, - details: { routePath }, - }; - } + // Sanitize parameters (remove undefined values) + const sanitizedParams = Object.fromEntries( + Object.entries(params).map(([key, value]) => [key, value ?? ""]), + ); - const query: Record = {}; - if (queryString) { - new URLSearchParams(queryString).forEach((value, key) => { - query[key] = value; + logger.info(`[DeepLink] ๐Ÿงน Parameters sanitized:`, sanitizedParams); + + await this.validateAndRoute(path, sanitizedParams, query); + 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 = {}; - if (pathParams) { - // Now we know routePath exists in ROUTE_MAP - const routeConfig = ROUTE_MAP[routePath]; - params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); + const deepLinkError = error as DeepLinkError; + throw deepLinkError; } + } - // logConsoleAndDb( - // `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`, - // false, - // ); - return { path: routePath, params, query }; + /** + * Parse a deep link URL into its components + * @param url - The deep link URL + * @returns Parsed components + */ + private parseDeepLink(url: string): { + path: string; + params: Record; + query: Record; + } { + 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 = {}; + if (pathParams.length > 0) { + params.id = pathParams[0]; + logger.debug( + `[DeepLink] ๐Ÿ“ Path parameter extracted: id=${pathParams[0]}`, + ); + } + + // Parse query parameters + const query: Record = {}; + 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. - * Validates route and parameters using Zod schemas before routing. - * - * @param path - The route path from the deep link - * @param params - URL parameters - * @param query - Query string parameters - * @throws {DeepLinkError} If validation fails or route is invalid + * Validate and route the deep link + * @param path - The route path + * @param params - Path parameters + * @param query - Query parameters */ private async validateAndRoute( path: string, params: Record, query: Record, ): Promise { + logger.info( + `[DeepLink] ๐ŸŽฏ Starting validation and routing for path: ${path}`, + ); + // First try to validate the route path let routeName: string; try { + logger.debug(`[DeepLink] ๐Ÿ” Validating route path: ${path}`); // Validate route exists 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) { - 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 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; } - // Continue with parameter validation as before... + // Continue with parameter validation + logger.info( + `[DeepLink] ๐Ÿ” Starting parameter validation for route: ${routeName}`, + ); + const pathSchema = deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas]; const querySchema = 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 = {}; let validatedQueryParams: Record = {}; + try { if (pathSchema) { + logger.debug(`[DeepLink] ๐Ÿ” Validating path parameters:`, 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) { + logger.debug(`[DeepLink] ๐Ÿ” Validating query parameters:`, 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) { - // For parameter validation errors, provide specific error feedback - logger.error( - `[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)}`, - ); + logger.error(`[DeepLink] โŒ Parameter validation failed:`, { + routeName, + path, + params, + query, + error: error instanceof Error ? error.message : String(error), + errorDetails: JSON.stringify(error), + }); + await this.router.replace({ name: "deep-link-error", 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; } + // Attempt navigation try { + logger.info(`[DeepLink] ๐Ÿš€ Attempting navigation:`, { + routeName, + pathParams: validatedPathParams, + queryParams: validatedQueryParams, + }); + await this.router.replace({ name: routeName, params: validatedPathParams, query: validatedQueryParams, }); + + logger.info(`[DeepLink] โœ… Navigation successful to: ${routeName}`); } catch (error) { - logger.error( - `[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedPathParams)} ... and query: ${JSON.stringify(validatedQueryParams)}`, - ); - // For parameter validation errors, provide specific error feedback + logger.error(`[DeepLink] โŒ Navigation failed:`, { + routeName, + path, + 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({ name: "deep-link-error", params: validatedPathParams, query: { originalPath: path, errorCode: "ROUTING_ERROR", - errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`, + errorMessage: `Error routing to ${routeName}: ${(error as Error).message}`, ...validatedQueryParams, }, }); - } - } - /** - * Processes incoming deep links and routes them appropriately. - * 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 { - try { - const { path, params, query } = this.parseDeepLink(url); - // Ensure params is always a Record 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}`, + logger.info( + `[DeepLink] ๐Ÿ”„ Redirected to error page for navigation failure`, ); - - throw { - code: deepLinkError.code || "UNKNOWN_ERROR", - message: deepLinkError.message, - details: deepLinkError.details, - }; } } }