/** * 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 * @version 2.0.0 * @since 2025-01-25 */ import { Router } from "vue-router"; import { z } from "zod"; import { deepLinkPathSchemas, routeSchema, DeepLinkRoute, deepLinkQuerySchemas, } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks"; import { logger } from "../utils/logger"; // Helper function to extract the first key from a Zod object schema function getFirstKeyFromZodObject( // eslint-disable-next-line @typescript-eslint/no-explicit-any schema: z.ZodObject, ): string | undefined { const shape = schema.shape; const keys = Object.keys(shape); return keys.length > 0 ? keys[0] : undefined; } /** * Maps deep link routes to their corresponding Vue router names and optional parameter keys. * * It's an object where keys are the deep link routes and values are objects with 'name' and 'paramKey'. * * The paramKey is used to extract the parameter from the route path, * because "router.replace" expects the right parameter name for the route. */ export const ROUTE_MAP: Record = Object.entries(deepLinkPathSchemas).reduce( (acc, [routeName, schema]) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject); acc[routeName] = { name: routeName, paramKey, }; return acc; }, {} as Record, ); /** * Handles processing and routing of deep links in the application. * Provides validation, error handling, and routing for deep link URLs. */ export class DeepLinkHandler { private router: Router; /** * Creates a new DeepLinkHandler instance. * @param router - Vue Router instance for navigation */ constructor(router: Router) { this.router = router; } /** * Main entry point for processing deep links * @param url - The deep link URL to process * @throws {DeepLinkError} If validation fails or route is invalid */ async handleDeepLink(url: string): Promise { logger.info(`[DeepLink] ๐Ÿš€ Starting deeplink processing for URL: ${url}`); try { logger.info(`[DeepLink] ๐Ÿ“ Parsing URL: ${url}`); const { path, params, query } = this.parseDeepLink(url); logger.info(`[DeepLink] โœ… URL parsed successfully:`, { path, params: Object.keys(params), query: Object.keys(query), fullParams: params, fullQuery: query, }); // Sanitize parameters (remove undefined values) const sanitizedParams = Object.fromEntries( Object.entries(params).map(([key, value]) => [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 deepLinkError = error as DeepLinkError; throw deepLinkError; } } /** * 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 using route-specific configuration const params: Record = {}; if (pathParams.length > 0) { // Get the correct parameter key for this route const routeConfig = ROUTE_MAP[routePath]; if (routeConfig?.paramKey) { params[routeConfig.paramKey] = pathParams[0]; logger.debug( `[DeepLink] ๐Ÿ“ Path parameter extracted: ${routeConfig.paramKey}=${pathParams[0]}`, ); } else { // Fallback to 'id' for backward compatibility params.id = pathParams[0]; logger.debug( `[DeepLink] ๐Ÿ“ Path parameter extracted: id=${pathParams[0]} (fallback)`, ); } } // 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; } } /** * 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; 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] โŒ 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({ name: "deep-link-error", params, query: { originalPath: path, errorCode: "INVALID_ROUTE", errorMessage: `The link you followed (${path}) is not supported`, ...query, }, }); logger.info( `[DeepLink] ๐Ÿ”„ Redirected to error page for invalid route: ${path}`, ); return; } // 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) { 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, query: { originalPath: path, errorCode: "INVALID_PARAMETERS", errorMessage: `The link parameters are invalid: ${(error as Error).message}`, ...query, }, }); 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] โŒ 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}: ${(error as Error).message}`, ...validatedQueryParams, }, }); logger.info( `[DeepLink] ๐Ÿ”„ Redirected to error page for navigation failure`, ); } } }