From 2660b9199595644f4e0638b1783a60e6b97d63cb Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Tue, 18 Mar 2025 09:19:35 +0000 Subject: [PATCH] wip: Improve deep link validation and error handling - Add comprehensive route validation with zod schema - Create type-safe DeepLinkRoute enum for all valid routes - Add structured error handling for invalid routes - Redirect to error page with detailed feedback - Add better timeout handling in deeplink tests The changes improve robustness by: 1. Validating route paths before navigation 2. Providing detailed error messages for invalid links 3. Redirecting users to dedicated error pages 4. Adding parameter validation with specific feedback 5. Improving type safety across deeplink handling --- scripts/test-android.js | 11 +++++- src/libs/util.ts | 1 - src/router/index.ts | 9 +++++ src/services/deepLinks.ts | 70 ++++++++++++++++++++++++++------- src/types/deepLinks.ts | 24 ++++++++++- src/views/DeepLinkErrorView.vue | 67 +++++++++++++++++++++++++++++++ src/views/ProjectViewView.vue | 12 ++++-- 7 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 src/views/DeepLinkErrorView.vue diff --git a/scripts/test-android.js b/scripts/test-android.js index ce117b6fa..cb7f1f585 100644 --- a/scripts/test-android.js +++ b/scripts/test-android.js @@ -122,8 +122,15 @@ const executeDeeplink = async (url, description, log) => { execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`); log(`✅ Successfully executed: ${description}`); - // Wait between deeplink tests - await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5s + // Wait for app to load content + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Press a key (Back button) to ensure app is in consistent state + log(`📱 Sending keystroke (BACK) to device...`); + execSync('adb shell input keyevent KEYCODE_BACK'); + + // Wait a bit longer after keystroke before next test + await new Promise(resolve => setTimeout(resolve, 2000)); } catch (error) { log(`❌ Failed to execute deeplink: ${description}`); log(`Error: ${error.message}`); diff --git a/src/libs/util.ts b/src/libs/util.ts index a249dcab1..1d2fa0311 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -99,7 +99,6 @@ export function numberOrZero(str: string): number { return isNumeric(str) ? +str : 0; } - /** * from https://tools.ietf.org/html/rfc3986#section-3 * also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition diff --git a/src/router/index.ts b/src/router/index.ts index e17801f5b..b28aa8e39 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -281,6 +281,15 @@ const routes: Array = [ name: "user-profile", component: () => import("../views/UserProfileView.vue"), }, + { + path: "/deep-link-error", + name: "deep-link-error", + component: () => import("../views/DeepLinkErrorView.vue"), + meta: { + title: "Invalid Deep Link", + requiresAuth: false, + }, + }, ]; const isElectron = window.location.protocol === "file:"; diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 8d75d0e6e..5a253f8a9 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -29,7 +29,11 @@ */ import { Router } from "vue-router"; -import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks"; +import { + deepLinkSchemas, + baseUrlSchema, + routeSchema, +} from "../types/deepLinks"; import { logConsoleAndDb } from "../db"; import type { DeepLinkError } from "../interfaces/deepLinks"; @@ -111,7 +115,7 @@ export class DeepLinkHandler { ): Promise { const routeMap: Record = { "user-profile": "user-profile", - project: "project", + "project-details": "project-details", "onboard-meeting-setup": "onboard-meeting-setup", "invite-one-accept": "invite-one-accept", "contact-import": "contact-import", @@ -124,25 +128,63 @@ export class DeepLinkHandler { did: "did", }; - const routeName = routeMap[path]; - if (!routeName) { + // First try to validate the route path + let routeName: string; + + try { + // Validate route exists + const validRoute = routeSchema.parse(path) as DeepLinkRoute; + routeName = routeMap[validRoute]; + } 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}`, }; } - // Validate parameters based on route type + // Continue with parameter validation as before... const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; - const validatedParams = await schema.parseAsync({ - ...params, - ...query, - }); - await this.router.replace({ - name: routeName, - params: validatedParams, - query, - }); + 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, + }; + } } } diff --git a/src/types/deepLinks.ts b/src/types/deepLinks.ts index 923027c2a..a8564d6d8 100644 --- a/src/types/deepLinks.ts +++ b/src/types/deepLinks.ts @@ -27,13 +27,35 @@ */ import { z } from "zod"; -// Base URL validation schema +// Add a union type of all valid route paths +export const VALID_DEEP_LINK_ROUTES = [ + "user-profile", + "project-details", + "onboard-meeting-setup", + "invite-one-accept", + "contact-import", + "confirm-gift", + "claim", + "claim-cert", + "claim-add-raw", + "contact-edit", + "contacts", + "did", +] 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 = { "user-profile": z.object({ diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue new file mode 100644 index 000000000..2fba6e4f6 --- /dev/null +++ b/src/views/DeepLinkErrorView.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index c38df85a2..95476afb3 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -372,7 +372,10 @@
- +
Totals @@ -451,14 +454,17 @@
{{ give.issuedAt?.substring(0, 10) }} -
+
{{ give.description }}