Browse Source
			
			
			
			
				
		- Add type-safe deep link parameter validation using Zod - Implement consistent error handling across all deep link routes - Add support for query parameters in deep links - Create comprehensive deep linking documentation - Add logging for deep link operations Security: - Validate all deep link parameters before processing - Sanitize and type-check query parameters - Add error boundaries around deep link handling - Implement route-specific parameter validation Testing: - Add parameter validation tests - Add error handling tests - Test query parameter support
				 6 changed files with 199 additions and 85 deletions
			
			
		@ -0,0 +1,48 @@ | 
				
			|||
# TimeSafari Deep Linking | 
				
			|||
 | 
				
			|||
## Supported URL Schemes | 
				
			|||
 | 
				
			|||
All deep links follow the format: `timesafari://<route>/<param>?<query>` | 
				
			|||
 | 
				
			|||
### Claim Routes | 
				
			|||
 | 
				
			|||
- `timesafari://claim/:id` | 
				
			|||
  - Query params: | 
				
			|||
    - `view`: "details" | "certificate" | "raw" | 
				
			|||
 | 
				
			|||
- `timesafari://claim-cert/:id` | 
				
			|||
- `timesafari://claim-add-raw/:id` | 
				
			|||
  - Query params: | 
				
			|||
    - `claim`: JSON string of claim data | 
				
			|||
    - `claimJwtId`: JWT ID for claim | 
				
			|||
 | 
				
			|||
### Contact Routes | 
				
			|||
 | 
				
			|||
- `timesafari://contact-edit/:did` | 
				
			|||
- `timesafari://contact-import/:jwt` | 
				
			|||
  - Query params: | 
				
			|||
    - `contacts`: JSON array of contacts | 
				
			|||
 | 
				
			|||
### Project Routes | 
				
			|||
 | 
				
			|||
- `timesafari://project/:id` | 
				
			|||
  - Query params: | 
				
			|||
    - `view`: "details" | "edit" | 
				
			|||
 | 
				
			|||
### Invite Routes | 
				
			|||
 | 
				
			|||
- `timesafari://invite-one-accept/:jwt` | 
				
			|||
  - Query params: | 
				
			|||
    - `type`: "one" | "many" | 
				
			|||
 | 
				
			|||
### Gift Routes | 
				
			|||
 | 
				
			|||
- `timesafari://confirm-gift/:id` | 
				
			|||
  - Query params: | 
				
			|||
    - `action`: "confirm" | "details" | 
				
			|||
 | 
				
			|||
### Offer Routes | 
				
			|||
 | 
				
			|||
- `timesafari://offer-details/:id` | 
				
			|||
  - Query params: | 
				
			|||
    - `view`: "details" | 
				
			|||
@ -0,0 +1,84 @@ | 
				
			|||
import { Router } from "vue-router"; | 
				
			|||
import { deepLinkSchemas, DeepLinkParams } from "../types/deepLinks"; | 
				
			|||
import { logConsoleAndDb } from "../db"; | 
				
			|||
 | 
				
			|||
interface DeepLinkError extends Error { | 
				
			|||
  code: string; | 
				
			|||
  details?: unknown; | 
				
			|||
} | 
				
			|||
 | 
				
			|||
export class DeepLinkHandler { | 
				
			|||
  private router: Router; | 
				
			|||
 | 
				
			|||
  constructor(router: Router) { | 
				
			|||
    this.router = router; | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  /** | 
				
			|||
   * Processes incoming deep links and routes them appropriately | 
				
			|||
   * @param url The deep link URL to process | 
				
			|||
   */ | 
				
			|||
  async handleDeepLink(url: string): Promise<void> { | 
				
			|||
    try { | 
				
			|||
      logConsoleAndDb("[DeepLink] Processing URL: " + url, false); | 
				
			|||
       | 
				
			|||
      const { path, params, query } = this.parseDeepLink(url); | 
				
			|||
      await this.validateAndRoute(path, params, query); | 
				
			|||
 | 
				
			|||
    } catch (error) { | 
				
			|||
      const deepLinkError = error as DeepLinkError; | 
				
			|||
      logConsoleAndDb( | 
				
			|||
        `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`, | 
				
			|||
        true | 
				
			|||
      ); | 
				
			|||
       | 
				
			|||
      throw { | 
				
			|||
        code: deepLinkError.code || 'UNKNOWN_ERROR', | 
				
			|||
        message: deepLinkError.message, | 
				
			|||
        details: deepLinkError.details | 
				
			|||
      }; | 
				
			|||
    } | 
				
			|||
  } | 
				
			|||
 | 
				
			|||
  /** | 
				
			|||
   * Routes the deep link to appropriate view with validated parameters | 
				
			|||
   */ | 
				
			|||
  private async validateAndRoute( | 
				
			|||
    path: string, | 
				
			|||
    params: Record<string, string>, | 
				
			|||
    query: Record<string, string> | 
				
			|||
  ): Promise<void> { | 
				
			|||
    const routeMap: Record<string, string> = { | 
				
			|||
      claim: 'claim', | 
				
			|||
      'claim-cert': 'claim-cert', | 
				
			|||
      'claim-add-raw': 'claim-add-raw', | 
				
			|||
      'contact-edit': 'contact-edit', | 
				
			|||
      'contact-import': 'contact-import', | 
				
			|||
      project: 'project', | 
				
			|||
      'invite-one-accept': 'invite-one-accept', | 
				
			|||
      'offer-details': 'offer-details', | 
				
			|||
      'confirm-gift': 'confirm-gift' | 
				
			|||
    }; | 
				
			|||
 | 
				
			|||
    const routeName = routeMap[path]; | 
				
			|||
    if (!routeName) { | 
				
			|||
      throw { | 
				
			|||
        code: 'INVALID_ROUTE', | 
				
			|||
        message: `Unsupported route: ${path}` | 
				
			|||
      }; | 
				
			|||
    } | 
				
			|||
 | 
				
			|||
    // Validate parameters based on route type
 | 
				
			|||
    const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; | 
				
			|||
    const validatedParams = await schema.parseAsync({ | 
				
			|||
      ...params, | 
				
			|||
      ...query | 
				
			|||
    }); | 
				
			|||
 | 
				
			|||
    await this.router.replace({ | 
				
			|||
      name: routeName, | 
				
			|||
      params: validatedParams, | 
				
			|||
      query | 
				
			|||
    }); | 
				
			|||
  } | 
				
			|||
}  | 
				
			|||
@ -0,0 +1,46 @@ | 
				
			|||
import { z } from "zod"; | 
				
			|||
 | 
				
			|||
// Base URL validation schema
 | 
				
			|||
const baseUrlSchema = z.object({ | 
				
			|||
  scheme: z.literal("timesafari"), | 
				
			|||
  path: z.string(), | 
				
			|||
  queryParams: z.record(z.string()).optional() | 
				
			|||
}); | 
				
			|||
 | 
				
			|||
// Parameter validation schemas for each route type
 | 
				
			|||
export const deepLinkSchemas = { | 
				
			|||
  claim: z.object({ | 
				
			|||
    id: z.string().min(1), | 
				
			|||
    view: z.enum(["details", "certificate", "raw"]).optional() | 
				
			|||
  }), | 
				
			|||
   | 
				
			|||
  contact: z.object({ | 
				
			|||
    did: z.string().regex(/^did:/), | 
				
			|||
    action: z.enum(["edit", "import"]).optional(), | 
				
			|||
    jwt: z.string().optional() | 
				
			|||
  }), | 
				
			|||
 | 
				
			|||
  project: z.object({ | 
				
			|||
    id: z.string().min(1), | 
				
			|||
    view: z.enum(["details", "edit"]).optional() | 
				
			|||
  }), | 
				
			|||
 | 
				
			|||
  invite: z.object({ | 
				
			|||
    jwt: z.string().min(1), | 
				
			|||
    type: z.enum(["one", "many"]).optional() | 
				
			|||
  }), | 
				
			|||
 | 
				
			|||
  gift: z.object({ | 
				
			|||
    id: z.string().min(1), | 
				
			|||
    action: z.enum(["confirm", "details"]).optional() | 
				
			|||
  }), | 
				
			|||
 | 
				
			|||
  offer: z.object({ | 
				
			|||
    id: z.string().min(1), | 
				
			|||
    view: z.enum(["details"]).optional() | 
				
			|||
  }) | 
				
			|||
}; | 
				
			|||
 | 
				
			|||
export type DeepLinkParams = { | 
				
			|||
  [K in keyof typeof deepLinkSchemas]: z.infer<typeof deepLinkSchemas[K]>; | 
				
			|||
};  | 
				
			|||
					Loading…
					
					
				
		Reference in new issue