@ -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 ,
} ;
}
}
}
}
}
}