diff --git a/doc/DEEP_LINKS.md b/doc/DEEP_LINKS.md index a68a5ed1..a6bf9f6b 100644 --- a/doc/DEEP_LINKS.md +++ b/doc/DEEP_LINKS.md @@ -100,6 +100,7 @@ try { - `src/interfaces/deepLinks.ts`: Type definitions and validation schemas - `src/services/deepLinks.ts`: Deep link processing service - `src/main.capacitor.ts`: Capacitor integration +- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web ## Type Safety Examples diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index c8688b83..1d9ab4bc 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -219,9 +219,9 @@ export async function logConsoleAndDb( isError = false, ): Promise { if (isError) { - logger.error(`${new Date().toISOString()} ${message}`); + logger.error(`${new Date().toISOString()}`, message); } else { - logger.log(`${new Date().toISOString()} ${message}`); + logger.log(`${new Date().toISOString()}`, message); } await logToDb(message); } diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index b56862c1..a275eecf 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -29,18 +29,17 @@ import { z } from "zod"; // Add a union type of all valid route paths export const VALID_DEEP_LINK_ROUTES = [ - "user-profile", - "project", - "onboard-meeting-setup", - "invite-one-accept", - "contact-import", - "confirm-gift", + // note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts "claim", - "claim-cert", "claim-add-raw", - "contact-edit", - "contacts", + "claim-cert", + "confirm-gift", + "contact-import", "did", + "invite-one-accept", + "onboard-meeting-setup", + "project", + "user-profile", ] as const; // Create a type from the array @@ -58,43 +57,38 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES); // Parameter validation schemas for each route type export const deepLinkSchemas = { - "user-profile": z.object({ + // note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts + claim: z.object({ id: z.string(), }), - project: z.object({ + "claim-add-raw": z.object({ id: z.string(), + claim: z.string().optional(), + claimJwtId: z.string().optional(), }), - "onboard-meeting-setup": z.object({ + "claim-cert": z.object({ id: z.string(), }), - "invite-one-accept": z.object({ + "confirm-gift": z.object({ id: z.string(), }), "contact-import": z.object({ jwt: z.string(), }), - "confirm-gift": z.object({ - id: z.string(), + did: z.object({ + did: z.string(), }), - claim: z.object({ - id: z.string(), + "invite-one-accept": z.object({ + jwt: z.string(), }), - "claim-cert": z.object({ + "onboard-meeting-setup": z.object({ id: z.string(), }), - "claim-add-raw": z.object({ + project: z.object({ id: z.string(), - claim: z.string().optional(), - claimJwtId: z.string().optional(), - }), - "contact-edit": z.object({ - did: z.string(), }), - contacts: z.object({ - contacts: z.string(), // JSON string of contacts array - }), - did: z.object({ - did: z.string(), + "user-profile": z.object({ + id: z.string(), }), }; diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index dc4074c4..3ac12d1f 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -34,8 +34,7 @@ import router from "./router"; import { handleApiError } from "./services/api"; import { AxiosError } from "axios"; import { DeepLinkHandler } from "./services/deepLinks"; -import { logConsoleAndDb } from "./db/databaseUtil"; -import { logger } from "./utils/logger"; +import { logger, safeStringify } from "./utils/logger"; logger.log("[Capacitor] Starting initialization"); logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM); @@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => { await router.isReady(); await deepLinkHandler.handleDeepLink(data.url); } catch (error) { - logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true); + logger.error("[DeepLink] Error handling deep link: ", error); handleApiError( { - message: error instanceof Error ? error.message : String(error), + message: error instanceof Error ? error.message : safeStringify(error), } as AxiosError, "deep-link", ); diff --git a/src/router/index.ts b/src/router/index.ts index 0b9aa52b..fabce1b5 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -83,6 +83,11 @@ const routes: Array = [ name: "discover", component: () => import("../views/DiscoverView.vue"), }, + { + path: "/deep-link/:path*", + name: "deep-link", + component: () => import("../views/DeepLinkRedirectView.vue"), + }, { path: "/gifted-details", name: "gifted-details", diff --git a/src/services/api.ts b/src/services/api.ts index 3235100e..d7b67beb 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -6,7 +6,7 @@ */ import { AxiosError } from "axios"; -import { logger } from "../utils/logger"; +import { logger, safeStringify } from "../utils/logger"; /** * Handles API errors with platform-specific logging and error processing. @@ -37,7 +37,8 @@ import { logger } from "../utils/logger"; */ export const handleApiError = (error: AxiosError, endpoint: string) => { if (process.env.VITE_PLATFORM === "capacitor") { - logger.error(`[Capacitor API Error] ${endpoint}:`, { + const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links + logger.error(`[Capacitor API Error] ${endpointStr}:`, { message: error.message, status: error.response?.status, data: error.response?.data, diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index e9f63a88..93fe9aa3 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -27,18 +27,16 @@ * timesafari://[/][?queryParam1=value1&queryParam2=value2] * * Supported Routes: - * - user-profile: View user profile - * - project: 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 + * - claim-cert: View claim certificate + * - confirm-gift + * - contact-import: Import contacts * - did: View DID + * - invite-one-accept: Accept invitation + * - onboard-meeting-members + * - project: View project details + * - user-profile: View user profile * * @example * const handler = new DeepLinkHandler(router); @@ -54,6 +52,7 @@ import { } from "../interfaces/deepLinks"; import { logConsoleAndDb } from "../db/databaseUtil"; import type { DeepLinkError } from "../interfaces/deepLinks"; +import { logger } from "@/utils/logger"; /** * Handles processing and routing of deep links in the application. @@ -81,14 +80,15 @@ export class DeepLinkHandler { 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" }, - "onboard-meeting-setup": { name: "onboard-meeting-setup" }, project: { name: "project" }, "user-profile": { name: "user-profile" }, }; @@ -99,7 +99,7 @@ export class DeepLinkHandler { * * @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) + * @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string}) */ private parseDeepLink(url: string) { const parts = url.split("://"); @@ -115,7 +115,16 @@ export class DeepLinkHandler { }); const [path, queryString] = parts[1].split("?"); - const [routePath, param] = path.split("/"); + const [routePath, ...pathParams] = path.split("/"); + // logger.log( + // "[DeepLink] Debug:", + // "Route Path:", + // routePath, + // "Path Params:", + // pathParams, + // "Query String:", + // queryString, + // ); // Validate route exists before proceeding if (!this.ROUTE_MAP[routePath]) { @@ -134,10 +143,10 @@ export class DeepLinkHandler { } const params: Record = {}; - if (param) { + if (pathParams) { // Now we know routePath exists in ROUTE_MAP const routeConfig = this.ROUTE_MAP[routePath]; - params[routeConfig.paramKey ?? "id"] = param; + params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); } return { path: routePath, params, query }; } @@ -243,6 +252,8 @@ export class DeepLinkHandler { code: "INVALID_PARAMETERS", message: (error as Error).message, details: error, + params: params, + query: query, }; } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2cbb228b..89425d77 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,6 +1,6 @@ import { logToDb } from "../db/databaseUtil"; -function safeStringify(obj: unknown) { +export function safeStringify(obj: unknown) { const seen = new WeakSet(); return JSON.stringify(obj, (_key, value) => { @@ -67,8 +67,9 @@ export const logger = { // Errors will always be logged // eslint-disable-next-line no-console console.error(message, ...args); - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); + const messageString = safeStringify(message); + const argsString = args.length > 0 ? safeStringify(args) : ""; + logToDb(messageString + argsString); }, }; diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index cdbadb00..02f8c6a4 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -1397,7 +1397,8 @@ export default class ContactsView extends Vue { const contactsJwt = await createEndorserJwtForDid(this.activeDid, { contacts: selectedContacts, }); - const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt; + const contactsJwtUrl = + APP_SERVER + "/deep-link/contact-import/" + contactsJwt; useClipboard() .copy(contactsJwtUrl) .then(() => { diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue index 406f0b5c..e65d3b58 100644 --- a/src/views/DeepLinkErrorView.vue +++ b/src/views/DeepLinkErrorView.vue @@ -66,9 +66,14 @@ const formattedPath = computed(() => { const path = originalPath.value.replace(/^\/+/, ""); // Log for debugging - logger.log("Original Path:", originalPath.value); - logger.log("Route Params:", route.params); - logger.log("Route Query:", route.query); + logger.log( + "[DeepLinkError] Original Path:", + originalPath.value, + "Route Params:", + route.params, + "Route Query:", + route.query, + ); return path; }); diff --git a/src/views/DeepLinkRedirectView.vue b/src/views/DeepLinkRedirectView.vue new file mode 100644 index 00000000..1615cfd4 --- /dev/null +++ b/src/views/DeepLinkRedirectView.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 70a569a2..b6ea5378 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -519,7 +519,6 @@ export default class HomeView extends Vue { // Retrieve DIDs with better error handling try { this.allMyDids = await retrieveAccountDids(); - logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`); } catch (error) { logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true); throw new Error( @@ -552,9 +551,6 @@ export default class HomeView extends Vue { if (USE_DEXIE_DB) { settings = await retrieveSettingsForActiveAccount(); } - logConsoleAndDb( - `[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`, - ); } catch (error) { logConsoleAndDb( `[HomeView] Failed to retrieve settings: ${error}`, @@ -581,9 +577,6 @@ export default class HomeView extends Vue { if (USE_DEXIE_DB) { this.allContacts = await db.contacts.toArray(); } - logConsoleAndDb( - `[HomeView] Retrieved ${this.allContacts.length} contacts`, - ); } catch (error) { logConsoleAndDb( `[HomeView] Failed to retrieve contacts: ${error}`, @@ -641,9 +634,6 @@ export default class HomeView extends Vue { }); } this.isRegistered = true; - logConsoleAndDb( - `[HomeView] User ${this.activeDid} is now registered`, - ); } } catch (error) { logConsoleAndDb( @@ -685,11 +675,6 @@ export default class HomeView extends Vue { this.newOffersToUserHitLimit = offersToUser.hitLimit; this.numNewOffersToUserProjects = offersToProjects.data.length; this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit; - - logConsoleAndDb( - `[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` + - `${this.numNewOffersToUserProjects} project offers`, - ); } } catch (error) { logConsoleAndDb(