From 861408c7bc38fa16d17658d60e3024f2b7b7593f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 3 Jul 2025 17:01:08 -0600 Subject: [PATCH] Consolidate deep-link paths to be derived from the same source so they don't get out of sync any more. --- src/interfaces/deepLinks.ts | 53 ++++++++++++++----------------- src/services/deepLinks.ts | 56 ++++++++++++++++++--------------- src/views/DeepLinkErrorView.vue | 2 +- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index a275eecf..d9dbdbcc 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -27,37 +27,8 @@ */ import { z } from "zod"; -// Add a union type of all valid route paths -export const VALID_DEEP_LINK_ROUTES = [ - // note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts - "claim", - "claim-add-raw", - "claim-cert", - "confirm-gift", - "contact-import", - "did", - "invite-one-accept", - "onboard-meeting-setup", - "project", - "user-profile", -] as const; - -// Create a type from the array -export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; - -// Update your schema definitions to use this type -export const baseUrlSchema = z.object({ - scheme: z.literal("timesafari"), - path: z.string(), - queryParams: z.record(z.string()).optional(), -}); - -// Use the type to ensure route validation -export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES); - // Parameter validation schemas for each route type export const deepLinkSchemas = { - // note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts claim: z.object({ id: z.string(), }), @@ -72,16 +43,22 @@ export const deepLinkSchemas = { "confirm-gift": z.object({ id: z.string(), }), + "contact-edit": z.object({ + did: z.string(), + }), "contact-import": z.object({ jwt: z.string(), }), + contacts: z.object({ + contacts: z.string(), // JSON string of contacts array + }), did: z.object({ did: z.string(), }), "invite-one-accept": z.object({ jwt: z.string(), }), - "onboard-meeting-setup": z.object({ + "onboard-meeting-members": z.object({ id: z.string(), }), project: z.object({ @@ -92,6 +69,19 @@ export const deepLinkSchemas = { }), }; +// Create a type from the array +export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; + +// Update your schema definitions to use this type +export const baseUrlSchema = z.object({ + scheme: z.literal("timesafari"), + path: z.string(), + queryParams: z.record(z.string()).optional(), +}); + +// Add a union type of all valid route paths +export const VALID_DEEP_LINK_ROUTES = Object.keys(deepLinkSchemas) as readonly (keyof typeof deepLinkSchemas)[]; + export type DeepLinkParams = { [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; }; @@ -100,3 +90,6 @@ export interface DeepLinkError extends Error { code: string; details?: unknown; } + +// Use the type to ensure route validation +export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES as [string, ...string[]]); diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index bff3346c..34d35cbb 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -44,6 +44,8 @@ */ import { Router } from "vue-router"; +import { z } from "zod"; + import { deepLinkSchemas, baseUrlSchema, @@ -53,6 +55,31 @@ import { import { logConsoleAndDb } from "../db/databaseUtil"; import type { DeepLinkError } from "../interfaces/deepLinks"; +// Helper function to extract the first key from a Zod object schema +function getFirstKeyFromZodObject(schema: z.ZodObject): string | undefined { + const shape = schema.shape; + const keys = Object.keys(shape); + return keys.length > 0 ? keys[0] : undefined; +} + +/** + * Maps deep link routes to their corresponding Vue router names and optional parameter keys. + * + * It's an object where keys are the deep link routes and values are objects with 'name' and 'paramKey'. + * + * The paramKey is used to extract the parameter from the route path, + * because "router.replace" expects the right parameter name for the route. + */ +export const ROUTE_MAP: Record = + Object.entries(deepLinkSchemas).reduce((acc, [routeName, schema]) => { + const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject); + acc[routeName] = { + name: routeName, + paramKey + }; + return acc; + }, {} as Record); + /** * Handles processing and routing of deep links in the application. * Provides validation, error handling, and routing for deep link URLs. @@ -69,30 +96,7 @@ export class DeepLinkHandler { } /** - * 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 } - > = { - // note that similar lists are in src/interfaces/deepLinks.ts - claim: { name: "claim" }, - "claim-add-raw": { name: "claim-add-raw" }, - "claim-cert": { name: "claim-cert" }, - "confirm-gift": { name: "confirm-gift" }, - "contact-import": { name: "contact-import", paramKey: "jwt" }, - did: { name: "did", paramKey: "did" }, - "invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" }, - "onboard-meeting-members": { name: "onboard-meeting-members" }, - project: { name: "project" }, - "user-profile": { name: "user-profile" }, - }; - /** * Parses deep link URL into path, params and query components. * Validates URL structure using Zod schemas. * @@ -126,7 +130,7 @@ export class DeepLinkHandler { // ); // Validate route exists before proceeding - if (!this.ROUTE_MAP[routePath]) { + if (!ROUTE_MAP[routePath]) { throw { code: "INVALID_ROUTE", message: `Invalid route path: ${routePath}`, @@ -144,7 +148,7 @@ export class DeepLinkHandler { const params: Record = {}; if (pathParams) { // Now we know routePath exists in ROUTE_MAP - const routeConfig = this.ROUTE_MAP[routePath]; + const routeConfig = ROUTE_MAP[routePath]; params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); } return { path: routePath, params, query }; @@ -170,7 +174,7 @@ export class DeepLinkHandler { try { // Validate route exists const validRoute = routeSchema.parse(path) as DeepLinkRoute; - routeName = this.ROUTE_MAP[validRoute].name; + routeName = ROUTE_MAP[validRoute].name; } catch (error) { // Log the invalid route attempt logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true); diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue index e65d3b58..3abb3ea1 100644 --- a/src/views/DeepLinkErrorView.vue +++ b/src/views/DeepLinkErrorView.vue @@ -93,7 +93,7 @@ const reportIssue = () => { // Log the error for analytics onMounted(() => { logConsoleAndDb( - `[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}`, + `[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`, true, ); });