feat(deepLinks): implement comprehensive deep linking system

- 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
This commit is contained in:
Matthew Raymer
2025-02-26 09:35:04 +00:00
parent 3b4f4dc125
commit 1a9c97fe88
6 changed files with 199 additions and 85 deletions

View File

@@ -49,6 +49,8 @@ import { App } from "./lib/capacitor/app";
import router from "./router";
import { handleApiError } from "./services/api";
import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks";
import { logConsoleAndDb } from "./db";
console.log("[Capacitor] Starting initialization");
console.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
@@ -62,6 +64,8 @@ window.addEventListener("unhandledrejection", (event) => {
}
});
const deepLinkHandler = new DeepLinkHandler(router);
/**
* Handles deep link routing for the application
* Processes URLs in the format timesafari://<route>/<param>
@@ -80,56 +84,11 @@ window.addEventListener("unhandledrejection", (event) => {
*/
const handleDeepLink = async (data: { url: string }) => {
try {
console.log("[Capacitor Deep Link] START Handler");
console.log("[Capacitor Deep Link] Received URL:", data.url);
await router.isReady();
const parts = data.url.split("://");
if (parts.length !== 2) {
throw new Error("Invalid URL format");
}
const path = parts[1];
console.log("[Capacitor Deep Link] Parsed path:", path);
// Define supported parameterized routes and their regex patterns
const paramRoutes = {
"claim-add-raw": /^claim-add-raw\/(.+)$/,
"claim-cert": /^claim-cert\/(.+)$/,
claim: /^claim\/(.+)$/,
"confirm-gift": /^confirm-gift\/(.+)$/,
"contact-edit": /^contact-edit\/(.+)$/,
"contact-import": /^contact-import\/(.+)$/,
did: /^did\/(.+)$/,
"invite-one-accept": /^invite-one-accept\/(.+)$/,
"offer-details": /^offer-details\/(.+)$/,
project: /^project\/(.+)$/,
"user-profile": /^user-profile\/(.+)$/,
};
// Match route pattern and extract parameter
for (const [routeName, pattern] of Object.entries(paramRoutes)) {
const match = path.match(pattern);
if (match) {
console.log(
`[Capacitor Deep Link] Matched route: ${routeName}, param: ${match[1]}`,
);
await router.replace({
name: routeName,
params: { id: match[1] },
});
return;
}
}
// Default fallback for non-parameterized routes
await router.replace("/" + path);
await deepLinkHandler.handleDeepLink(data.url);
} catch (error) {
console.error("[Capacitor Deep Link] Error:", error);
if (error instanceof Error) {
handleApiError({ message: error.message } as AxiosError, "deep-link");
}
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
handleApiError(error, "deep-link");
}
};

84
src/services/deepLinks.ts Normal file
View File

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

46
src/types/deepLinks.ts Normal file
View File

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