forked from jsnbuchanan/crowd-funder-for-time-pwa
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
This commit is contained in:
@@ -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`,
|
||||||
|
);
|
||||||
|
|
||||||
logger.log("[Capacitor] Mounting app");
|
// 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,46 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* @file Deep Link Handler Service
|
* DeepLinks Service
|
||||||
|
*
|
||||||
|
* Handles deep link processing and routing for the TimeSafari application.
|
||||||
|
* Supports both path parameters and query parameters with comprehensive validation.
|
||||||
|
*
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
*
|
* @version 2.0.0
|
||||||
* This service handles the processing and routing of deep links in the TimeSafari app.
|
* @since 2025-01-25
|
||||||
* 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:
|
|
||||||
* - 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
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* const handler = new DeepLinkHandler(router);
|
|
||||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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),
|
||||||
// Validate route exists before proceeding
|
query: Object.keys(query),
|
||||||
if (!ROUTE_MAP[routePath]) {
|
fullParams: params,
|
||||||
throw {
|
fullQuery: query,
|
||||||
code: "INVALID_ROUTE",
|
|
||||||
message: `Invalid route path: ${routePath}`,
|
|
||||||
details: { routePath },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const query: Record<string, string> = {};
|
|
||||||
if (queryString) {
|
|
||||||
new URLSearchParams(queryString).forEach((value, key) => {
|
|
||||||
query[key] = value;
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const params: Record<string, string> = {};
|
// Sanitize parameters (remove undefined values)
|
||||||
if (pathParams) {
|
const sanitizedParams = Object.fromEntries(
|
||||||
// Now we know routePath exists in ROUTE_MAP
|
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||||
const routeConfig = ROUTE_MAP[routePath];
|
);
|
||||||
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
// logConsoleAndDb(
|
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
|
||||||
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`,
|
|
||||||
// false,
|
await this.validateAndRoute(path, sanitizedParams, query);
|
||||||
// );
|
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
|
||||||
return { path: routePath, params, query };
|
} 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 deepLinkError = error as DeepLinkError;
|
||||||
|
throw deepLinkError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes the deep link to appropriate view with validated parameters.
|
* Parse a deep link URL into its components
|
||||||
* Validates route and parameters using Zod schemas before routing.
|
* @param url - The deep link URL
|
||||||
*
|
* @returns Parsed components
|
||||||
* @param path - The route path from the deep link
|
*/
|
||||||
* @param params - URL parameters
|
private parseDeepLink(url: string): {
|
||||||
* @param query - Query string parameters
|
path: string;
|
||||||
* @throws {DeepLinkError} If validation fails or route is invalid
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and route the deep link
|
||||||
|
* @param path - The route path
|
||||||
|
* @param params - Path parameters
|
||||||
|
* @param query - Query parameters
|
||||||
*/
|
*/
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user