forked from jsnbuchanan/crowd-funder-for-time-pwa
- Move type definitions from src/types/ to src/interfaces/ for better organization - Enhance deep linking type system documentation with detailed examples - Update package dependencies to latest versions - Improve code organization in README.md - Fix formatting in WebPlatformService.ts This change consolidates all type definitions into the interfaces folder, improves type safety documentation, and updates dependencies for better maintainability. The deep linking system now has clearer documentation about its type system and validation approach. Breaking: Removes src/types/ directory in favor of src/interfaces/
245 lines
7.6 KiB
TypeScript
245 lines
7.6 KiB
TypeScript
/**
|
|
* @file Deep Link Handler 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:
|
|
* - user-profile: View user profile
|
|
* - project-details: View project details
|
|
* - onboard-meeting-setup: Setup onboarding meeting
|
|
* - invite-one-accept: Accept invitation
|
|
* - contact-import: Import contacts
|
|
* - confirm-gift: Confirm gift
|
|
* - claim: View claim
|
|
* - claim-cert: View claim certificate
|
|
* - claim-add-raw: Add raw claim
|
|
* - contact-edit: Edit contact
|
|
* - contacts: View contacts
|
|
* - did: View DID
|
|
*
|
|
* @example
|
|
* const handler = new DeepLinkHandler(router);
|
|
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
|
*/
|
|
|
|
import { Router } from "vue-router";
|
|
import {
|
|
deepLinkSchemas,
|
|
baseUrlSchema,
|
|
routeSchema,
|
|
DeepLinkRoute,
|
|
} from "../interfaces/deepLinks";
|
|
import { logConsoleAndDb } from "../db";
|
|
import type { DeepLinkError } from "../interfaces/deepLinks";
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Maps deep link routes to their corresponding Vue router names and optional parameter keys.
|
|
*
|
|
* The paramKey is used to extract the parameter from the route path,
|
|
* because "router.replace" expects the right parameter name for the route.
|
|
* The default is "id".
|
|
*/
|
|
private readonly ROUTE_MAP: Record<
|
|
string,
|
|
{ name: string; paramKey?: string }
|
|
> = {
|
|
"user-profile": { name: "user-profile" },
|
|
"project-details": { name: "project-details" },
|
|
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
|
"invite-one-accept": { name: "invite-one-accept" },
|
|
"contact-import": { name: "contact-import" },
|
|
"confirm-gift": { name: "confirm-gift" },
|
|
claim: { name: "claim" },
|
|
"claim-cert": { name: "claim-cert" },
|
|
"claim-add-raw": { name: "claim-add-raw" },
|
|
"contact-edit": { name: "contact-edit", paramKey: "did" },
|
|
contacts: { name: "contacts" },
|
|
did: { name: "did", paramKey: "did" },
|
|
};
|
|
|
|
/**
|
|
* Parses deep link URL into path, params and query components.
|
|
* Validates URL structure using Zod schemas.
|
|
*
|
|
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
|
* @throws {DeepLinkError} If URL format is invalid
|
|
* @returns Parsed URL components (path, params, query)
|
|
*/
|
|
private parseDeepLink(url: string) {
|
|
const parts = url.split("://");
|
|
if (parts.length !== 2) {
|
|
throw { code: "INVALID_URL", message: "Invalid URL format" };
|
|
}
|
|
|
|
// Validate base URL structure
|
|
baseUrlSchema.parse({
|
|
scheme: parts[0],
|
|
path: parts[1],
|
|
queryParams: {}, // Will be populated below
|
|
});
|
|
|
|
const [path, queryString] = parts[1].split("?");
|
|
const [routePath, param] = path.split("/");
|
|
|
|
const query: Record<string, string> = {};
|
|
if (queryString) {
|
|
new URLSearchParams(queryString).forEach((value, key) => {
|
|
query[key] = value;
|
|
});
|
|
}
|
|
|
|
const params: Record<string, string> = {};
|
|
if (param) {
|
|
if (this.ROUTE_MAP[routePath].paramKey) {
|
|
params[this.ROUTE_MAP[routePath].paramKey] = param;
|
|
} else {
|
|
params["id"] = param;
|
|
}
|
|
}
|
|
return { path: routePath, params, query };
|
|
}
|
|
|
|
/**
|
|
* Processes incoming deep links and routes them appropriately.
|
|
* 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 {
|
|
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
|
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;
|
|
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.
|
|
* Validates route and parameters using Zod schemas before routing.
|
|
*
|
|
* @param path - The route path from the deep link
|
|
* @param params - URL parameters
|
|
* @param query - Query string parameters
|
|
* @throws {DeepLinkError} If validation fails or route is invalid
|
|
*/
|
|
private async validateAndRoute(
|
|
path: string,
|
|
params: Record<string, string>,
|
|
query: Record<string, string>,
|
|
): Promise<void> {
|
|
// First try to validate the route path
|
|
let routeName: string;
|
|
|
|
try {
|
|
// Validate route exists
|
|
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
|
routeName = this.ROUTE_MAP[validRoute].name;
|
|
} catch (error) {
|
|
// Log the invalid route attempt
|
|
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
|
|
|
|
// Redirect to error page with information about the invalid link
|
|
await this.router.replace({
|
|
name: "deep-link-error",
|
|
query: {
|
|
originalPath: path,
|
|
errorCode: "INVALID_ROUTE",
|
|
message: `The link you followed (${path}) is not supported`,
|
|
},
|
|
});
|
|
|
|
throw {
|
|
code: "INVALID_ROUTE",
|
|
message: `Unsupported route: ${path}`,
|
|
};
|
|
}
|
|
|
|
// Continue with parameter validation as before...
|
|
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
|
|
|
try {
|
|
const validatedParams = await schema.parseAsync({
|
|
...params,
|
|
...query,
|
|
});
|
|
|
|
await this.router.replace({
|
|
name: routeName,
|
|
params: validatedParams,
|
|
query,
|
|
});
|
|
} catch (error) {
|
|
// For parameter validation errors, provide specific error feedback
|
|
await this.router.replace({
|
|
name: "deep-link-error",
|
|
query: {
|
|
originalPath: path,
|
|
errorCode: "INVALID_PARAMETERS",
|
|
message: `The link parameters are invalid: ${(error as Error).message}`,
|
|
},
|
|
});
|
|
|
|
throw {
|
|
code: "INVALID_PARAMETERS",
|
|
message: (error as Error).message,
|
|
details: error,
|
|
};
|
|
}
|
|
}
|
|
}
|