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 supportside_step
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