forked from trent_larson/crowd-funder-for-time-pwa
refactor: reorganize deep linking types and interfaces
Changes: - Move deep link types from types/ to interfaces/ - Export baseUrlSchema for external use - Add trailing commas for better git diffs - Fix type inference for deepLinkSchemas - Add deepLinks export to interfaces/index.ts - Remove duplicate SuccessResult interface - Update import paths in services/deepLinks.ts This improves code organization by centralizing interface definitions and fixing type inference issues.
This commit is contained in:
@@ -19,11 +19,6 @@ export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: unknown;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
|
||||
13
src/interfaces/deepLinks.ts
Normal file
13
src/interfaces/deepLinks.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @file Deep Link Interface Definitions
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* Defines the core interfaces for the deep linking system.
|
||||
* These interfaces are used across the deep linking implementation
|
||||
* to ensure type safety and consistent error handling.
|
||||
*/
|
||||
|
||||
export interface DeepLinkError extends Error {
|
||||
code: string;
|
||||
details?: unknown;
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from "./common";
|
||||
export * from "./limits";
|
||||
export * from "./records";
|
||||
export * from "./user";
|
||||
export * from "./deepLinks";
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
/**
|
||||
* @file Capacitor Main Entry Point
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
*
|
||||
* This file initializes the deep linking system for the TimeSafari app.
|
||||
* It sets up the connection between Capacitor's URL handling and our deep link processor.
|
||||
*
|
||||
*
|
||||
* Deep Linking Flow:
|
||||
* 1. Capacitor receives URL open event
|
||||
* 2. Event is passed to DeepLinkHandler
|
||||
* 3. URL is validated and processed
|
||||
* 4. Router navigates to appropriate view
|
||||
*
|
||||
*
|
||||
* Integration Points:
|
||||
* - Capacitor App plugin for URL handling
|
||||
* - Vue Router for navigation
|
||||
* - Error handling system
|
||||
* - Logging system
|
||||
*
|
||||
*
|
||||
* Type Safety:
|
||||
* - Uses DeepLinkHandler for type-safe parameter processing
|
||||
* - Ensures type safety between Capacitor events and app routing
|
||||
* - Maintains type checking through the entire deep link flow
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // URL open event from OS
|
||||
* timesafari://claim/123?view=details
|
||||
@@ -72,9 +72,12 @@ const handleDeepLink = async (data: { url: string }) => {
|
||||
await deepLinkHandler.handleDeepLink(data.url);
|
||||
} catch (error) {
|
||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
||||
handleApiError({
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
} as AxiosError, "deep-link");
|
||||
handleApiError(
|
||||
{
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
} as AxiosError,
|
||||
"deep-link",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
/**
|
||||
* @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 types/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
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* const handler = new DeepLinkHandler(router);
|
||||
* await handler.handleDeepLink("timesafari://claim/123?view=details");
|
||||
*/
|
||||
|
||||
import { Router } from "vue-router";
|
||||
import { deepLinkSchemas, DeepLinkParams } from "../types/deepLinks";
|
||||
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
|
||||
import { logConsoleAndDb } from "../db";
|
||||
|
||||
interface DeepLinkError extends Error {
|
||||
code: string;
|
||||
details?: unknown;
|
||||
}
|
||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||
|
||||
export class DeepLinkHandler {
|
||||
private router: Router;
|
||||
@@ -44,6 +40,39 @@ export class DeepLinkHandler {
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses deep link URL into path, params and query components
|
||||
*/
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
path: routePath,
|
||||
params: param ? { id: param } : {},
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes incoming deep links and routes them appropriately
|
||||
* @param url The deep link URL to process
|
||||
@@ -51,21 +80,23 @@ export class DeepLinkHandler {
|
||||
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);
|
||||
|
||||
// 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
|
||||
true,
|
||||
);
|
||||
|
||||
|
||||
throw {
|
||||
code: deepLinkError.code || 'UNKNOWN_ERROR',
|
||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||
message: deepLinkError.message,
|
||||
details: deepLinkError.details
|
||||
details: deepLinkError.details,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -76,25 +107,25 @@ export class DeepLinkHandler {
|
||||
private async validateAndRoute(
|
||||
path: string,
|
||||
params: Record<string, string>,
|
||||
query: 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'
|
||||
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}`
|
||||
code: "INVALID_ROUTE",
|
||||
message: `Unsupported route: ${path}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -102,13 +133,13 @@ export class DeepLinkHandler {
|
||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
||||
const validatedParams = await schema.parseAsync({
|
||||
...params,
|
||||
...query
|
||||
...query,
|
||||
});
|
||||
|
||||
await this.router.replace({
|
||||
name: routeName,
|
||||
params: validatedParams,
|
||||
query
|
||||
query,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
/**
|
||||
* @file Deep Link Type Definitions and Validation Schemas
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
*
|
||||
* This file defines the type system and validation schemas for deep linking in the TimeSafari app.
|
||||
* It uses Zod for runtime validation while providing TypeScript types for compile-time checking.
|
||||
*
|
||||
*
|
||||
* Type Strategy:
|
||||
* 1. Define base URL schema to validate the fundamental deep link structure
|
||||
* 2. Define route-specific parameter schemas with exact validation rules
|
||||
* 3. Generate TypeScript types from Zod schemas for type safety
|
||||
* 4. Export both schemas and types for use in deep link handling
|
||||
*
|
||||
*
|
||||
* Usage:
|
||||
* - Import schemas for runtime validation in deep link handlers
|
||||
* - Import types for type-safe parameter handling in components
|
||||
* - Use DeepLinkParams type for type-safe access to route parameters
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* // Runtime validation
|
||||
* const params = deepLinkSchemas.claim.parse({ id: "123", view: "details" });
|
||||
*
|
||||
*
|
||||
* // Type-safe parameter access
|
||||
* function handleClaimParams(params: DeepLinkParams["claim"]) {
|
||||
* // TypeScript knows params.id exists and params.view is optional
|
||||
@@ -28,46 +28,46 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Base URL validation schema
|
||||
const baseUrlSchema = z.object({
|
||||
export const baseUrlSchema = z.object({
|
||||
scheme: z.literal("timesafari"),
|
||||
path: z.string(),
|
||||
queryParams: z.record(z.string()).optional()
|
||||
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()
|
||||
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()
|
||||
jwt: z.string().optional(),
|
||||
}),
|
||||
|
||||
project: z.object({
|
||||
id: z.string().min(1),
|
||||
view: z.enum(["details", "edit"]).optional()
|
||||
view: z.enum(["details", "edit"]).optional(),
|
||||
}),
|
||||
|
||||
invite: z.object({
|
||||
jwt: z.string().min(1),
|
||||
type: z.enum(["one", "many"]).optional()
|
||||
type: z.enum(["one", "many"]).optional(),
|
||||
}),
|
||||
|
||||
gift: z.object({
|
||||
id: z.string().min(1),
|
||||
action: z.enum(["confirm", "details"]).optional()
|
||||
action: z.enum(["confirm", "details"]).optional(),
|
||||
}),
|
||||
|
||||
offer: z.object({
|
||||
id: z.string().min(1),
|
||||
view: z.enum(["details"]).optional()
|
||||
})
|
||||
view: z.enum(["details"]).optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
export type DeepLinkParams = {
|
||||
[K in keyof typeof deepLinkSchemas]: z.infer<typeof deepLinkSchemas[K]>;
|
||||
};
|
||||
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user