From 3fd6c2b80dbd7482dd922cc151566dfd8af9fd74 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 13:16:17 -0600 Subject: [PATCH 1/7] add first cut at deep-link redirecting, with one example contact-import that works on mobile --- doc/DEEP_LINKS.md | 1 + src/db/databaseUtil.ts | 4 +- src/interfaces/deepLinks.ts | 52 +++---- src/main.capacitor.ts | 7 +- src/router/index.ts | 5 + src/services/api.ts | 5 +- src/services/deepLinks.ts | 39 +++-- src/utils/logger.ts | 7 +- src/views/ContactsView.vue | 3 +- src/views/DeepLinkErrorView.vue | 11 +- src/views/DeepLinkRedirectView.vue | 221 +++++++++++++++++++++++++++++ src/views/HomeView.vue | 15 -- 12 files changed, 297 insertions(+), 73 deletions(-) create mode 100644 src/views/DeepLinkRedirectView.vue 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( -- 2.30.2 From 66895202701d4bb20a458acdf4e93cb6a6374d24 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 15:53:16 -0600 Subject: [PATCH 2/7] fix all copies for externally-shared links to redirected deep links --- src/components/HiddenDidDialog.vue | 15 ++++++--- src/libs/endorserServer.ts | 3 +- src/services/deepLinks.ts | 1 - src/views/ClaimView.vue | 33 +++++++++++++------ src/views/ConfirmGiftView.vue | 13 +++++--- src/views/ContactQRScanFullView.vue | 16 +++++++-- src/views/ContactQRScanShowView.vue | 17 ++++++++-- src/views/ContactsView.vue | 4 +-- src/views/InviteOneView.vue | 2 +- src/views/OnboardMeetingSetupView.vue | 2 +- src/views/ProjectViewView.vue | 47 ++++++++++++++++++++++++--- src/views/ShareMyContactInfoView.vue | 2 +- src/views/UserProfileView.vue | 33 +++++++++++++++++++ 13 files changed, 151 insertions(+), 37 deletions(-) diff --git a/src/components/HiddenDidDialog.vue b/src/components/HiddenDidDialog.vue index 980a3852..8593009e 100644 --- a/src/components/HiddenDidDialog.vue +++ b/src/components/HiddenDidDialog.vue @@ -77,7 +77,7 @@ If you'd like an introduction, click here to copy this page, paste it into a message, and ask if they'll tell you more about the {{ roleName }}. @@ -104,7 +104,7 @@ import * as R from "ramda"; import { useClipboard } from "@vueuse/core"; import { Contact } from "../db/tables/contacts"; import * as serverUtil from "../libs/endorserServer"; -import { NotificationIface } from "../constants/app"; +import { APP_SERVER, NotificationIface } from "../constants/app"; @Component export default class HiddenDidDialog extends Vue { @@ -117,7 +117,8 @@ export default class HiddenDidDialog extends Vue { activeDid = ""; allMyDids: Array = []; canShare = false; - windowLocation = window.location.href; + deepLinkPathSuffix = ""; + deepLinkUrl = window.location.href; // this is changed to a deep link in the setup R = R; serverUtil = serverUtil; @@ -129,17 +130,21 @@ export default class HiddenDidDialog extends Vue { } open( + deepLinkPathSuffix: string, roleName: string, visibleToDids: string[], allContacts: Array, activeDid: string, allMyDids: Array, ) { + this.deepLinkPathSuffix = deepLinkPathSuffix; this.roleName = roleName; this.visibleToDids = visibleToDids; this.allContacts = allContacts; this.activeDid = activeDid; this.allMyDids = allMyDids; + + this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix; this.isOpen = true; } @@ -173,11 +178,11 @@ export default class HiddenDidDialog extends Vue { } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.windowLocation); + this.copyToClipboard("A link to this page", this.deepLinkUrl); window.navigator.share({ title: "Help Connect Me", text: "I'm trying to find the people who recorded this. Can you help me?", - url: this.windowLocation, + url: this.deepLinkUrl, }); } } diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index cbdcbaee..f2dc79a4 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1074,7 +1074,8 @@ export async function generateEndorserJwtUrlForAccount( const vcJwt = await createEndorserJwtForDid(account.did, contactInfo); - const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI; + const viewPrefix = + APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI; return viewPrefix + vcJwt; } diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 93fe9aa3..8c8aa501 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -52,7 +52,6 @@ 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. diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 1d647d7a..8edf3bf8 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -49,21 +49,32 @@ v-if="veriClaim.id" :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)" class="text-blue-500 mt-2" - title="Printable Certificate" + title="View Printable Certificate" > +
@@ -405,7 +416,7 @@ contacts can see more details: click to copy this page info and see if they can make an introduction. Someone is connected to @@ -428,7 +439,7 @@ If you'd like an introduction, share this page with them and ask if they'll tell you more about about the participants. @@ -546,7 +557,7 @@ import { useClipboard } from "@vueuse/core"; import { GenericVerifiableCredential } from "../interfaces"; import GiftedDialog from "../components/GiftedDialog.vue"; import QuickNav from "../components/QuickNav.vue"; -import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; +import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import * as databaseUtil from "../db/databaseUtil"; import { db } from "../db/index"; import { logConsoleAndDb } from "../db/databaseUtil"; @@ -593,8 +604,9 @@ export default class ClaimView extends Vue { veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; veriClaimDump = ""; veriClaimDidsVisible: { [key: string]: string[] } = {}; - windowLocation = window.location.href; + windowDeepLink = window.location.href; // changed in the setup for deep linking + APP_SERVER = APP_SERVER; R = R; yaml = yaml; libsUtil = libsUtil; @@ -671,6 +683,7 @@ export default class ClaimView extends Vue { 5000, ); } + this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; this.canShare = !!navigator.share; } @@ -1006,11 +1019,11 @@ export default class ClaimView extends Vue { } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.windowLocation); + this.copyToClipboard("A link to this page", this.windowDeepLink); window.navigator.share({ title: "Help Connect Me", text: "I'm trying to find the people who recorded this. Can you help me?", - url: this.windowLocation, + url: this.windowDeepLink, }); } diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 63225259..793df516 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -436,7 +436,7 @@ import { Component, Vue } from "vue-facing-decorator"; import { useClipboard } from "@vueuse/core"; import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import QuickNav from "../components/QuickNav.vue"; -import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; +import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { Contact } from "../db/tables/contacts"; import * as databaseUtil from "../db/databaseUtil"; @@ -494,7 +494,7 @@ export default class ConfirmGiftView extends Vue { veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; veriClaimDump = ""; veriClaimDidsVisible: { [key: string]: string[] } = {}; - windowLocation = window.location.href; + windowLocation = window.location.href; // this is changed to a deep link in the setup R = R; yaml = yaml; @@ -566,6 +566,9 @@ export default class ConfirmGiftView extends Vue { } const claimId = decodeURIComponent(pathParam); + + this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId; + await this.loadClaim(claimId, this.activeDid); } @@ -676,12 +679,12 @@ export default class ConfirmGiftView extends Vue { /** * Add participant (giver/recipient) name & URL info */ - if (this.giveDetails?.agentDid) { - this.giverName = this.didInfo(this.giveDetails.agentDid); + this.giverName = this.didInfo(this.giveDetails?.agentDid); + if (this.giveDetails?.agentDid) { this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`; } + this.recipientName = this.didInfo(this.giveDetails?.recipientDid); if (this.giveDetails?.recipientDid) { - this.recipientName = this.didInfo(this.giveDetails.recipientDid); this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`; } diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index eba485ca..1ca08edc 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -124,12 +124,14 @@ import * as databaseUtil from "../db/databaseUtil"; import { CONTACT_CSV_HEADER, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + generateEndorserJwtUrlForAccount, setVisibilityUtil, } from "../libs/endorserServer"; import UserNameDialog from "../components/UserNameDialog.vue"; import { retrieveAccountMetadata } from "../libs/util"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { parseJsonField } from "../db/databaseUtil"; +import { Account } from "@/db/tables/accounts"; interface QRScanResult { rawValue?: string; @@ -157,6 +159,7 @@ export default class ContactQRScanFull extends Vue { apiServer = ""; givenName = ""; isRegistered = false; + profileImageUrl = ""; qrValue = ""; ETHR_DID_PREFIX = ETHR_DID_PREFIX; @@ -179,6 +182,7 @@ export default class ContactQRScanFull extends Vue { this.apiServer = settings.apiServer || ""; this.givenName = settings.firstName || ""; this.isRegistered = !!settings.isRegistered; + this.profileImageUrl = settings.profileImageUrl || ""; const account = await retrieveAccountMetadata(this.activeDid); if (account) { @@ -588,9 +592,17 @@ export default class ContactQRScanFull extends Vue { ); } - onCopyUrlToClipboard() { + async onCopyUrlToClipboard() { + const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const jwtUrl = await generateEndorserJwtUrlForAccount( + account, + this.isRegistered, + this.givenName, + this.profileImageUrl, + true, + ); useClipboard() - .copy(this.qrValue) + .copy(jwtUrl) .then(() => { this.$notify( { diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 076ee279..675559e7 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -177,6 +177,7 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { CONTACT_CSV_HEADER, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + generateEndorserJwtUrlForAccount, register, setVisibilityUtil, } from "../libs/endorserServer"; @@ -187,6 +188,7 @@ import { logger } from "../utils/logger"; import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory"; import { CameraState } from "@/services/QRScanner/types"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { Account } from "@/db/tables/accounts"; interface QRScanResult { rawValue?: string; @@ -216,6 +218,7 @@ export default class ContactQRScanShow extends Vue { isRegistered = false; qrValue = ""; isScanning = false; + profileImageUrl = ""; error: string | null = null; // QR Scanner properties @@ -253,6 +256,7 @@ export default class ContactQRScanShow extends Vue { this.hideRegisterPromptOnNewContact = !!settings.hideRegisterPromptOnNewContact; this.isRegistered = !!settings.isRegistered; + this.profileImageUrl = settings.profileImageUrl || ""; const account = await libsUtil.retrieveAccountMetadata(this.activeDid); if (account) { @@ -667,10 +671,17 @@ export default class ContactQRScanShow extends Vue { }); } - onCopyUrlToClipboard() { - //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing + async onCopyUrlToClipboard() { + const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const jwtUrl = await generateEndorserJwtUrlForAccount( + account, + this.isRegistered, + this.givenName, + this.profileImageUrl, + true, + ); useClipboard() - .copy(this.qrValue) + .copy(jwtUrl) .then(() => { this.$notify( { diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 02f8c6a4..faf63dbf 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -126,7 +126,6 @@
+
@@ -55,7 +61,11 @@ {{ issuerInfoObject?.displayName }} - + +
@@ -632,7 +642,7 @@ import TopMessage from "../components/TopMessage.vue"; import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; import ProjectIcon from "../components/ProjectIcon.vue"; -import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; +import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import * as databaseUtil from "../db/databaseUtil"; import { db, @@ -646,6 +656,7 @@ import { retrieveAccountDids } from "../libs/util"; import HiddenDidDialog from "../components/HiddenDidDialog.vue"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { useClipboard } from "@vueuse/core"; /** * Project View Component * @author Matthew Raymer @@ -842,6 +853,28 @@ export default class ProjectViewView extends Vue { }); } + onCopyLinkClick() { + const shortestProjectId = this.projectId.startsWith( + serverUtil.ENDORSER_CH_HANDLE_PREFIX, + ) + ? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length) + : this.projectId; + const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`; + useClipboard() + .copy(deepLink) + .then(() => { + this.$notify( + { + group: "alert", + type: "toast", + title: "Copied", + text: "A link to this project was copied to the clipboard.", + }, + 2000, + ); + }); + } + // Isn't there a better way to make this available to the template? expandText() { this.expanded = true; @@ -1304,7 +1337,7 @@ export default class ProjectViewView extends Vue { } // return an HTTPS URL if it's not a global URL - addScheme(url: string) { + ensureScheme(url: string) { if (!libsUtil.isGlobalUri(url)) { return "https://" + url; } @@ -1465,7 +1498,13 @@ export default class ProjectViewView extends Vue { } openHiddenDidDialog() { + const shortestProjectId = this.projectId.startsWith( + serverUtil.ENDORSER_CH_HANDLE_PREFIX, + ) + ? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length) + : this.projectId; (this.$refs.hiddenDidDialog as HiddenDidDialog).open( + "project/" + shortestProjectId, "creator", this.issuerVisibleToDids, this.allContacts, diff --git a/src/views/ShareMyContactInfoView.vue b/src/views/ShareMyContactInfoView.vue index 445c2775..ca1f2e94 100644 --- a/src/views/ShareMyContactInfoView.vue +++ b/src/views/ShareMyContactInfoView.vue @@ -105,7 +105,7 @@ export default class ShareMyContactInfoView extends Vue { group: "alert", type: "info", title: "Copied", - text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.", + text: "Your contact info was copied to the clipboard. Have them click on it, or paste it in the box on their 'Contacts' screen.", }, 5000, ); diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index 0e1471b8..11db4a09 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -16,6 +16,9 @@ Individual Profile +
+ +
@@ -32,6 +35,12 @@
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }} +

{{ profile.description }} @@ -100,6 +109,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router"; import QuickNav from "../components/QuickNav.vue"; import TopMessage from "../components/TopMessage.vue"; import { + APP_SERVER, DEFAULT_PARTNER_API_SERVER, NotificationIface, USE_DEXIE_DB, @@ -113,6 +123,7 @@ import { retrieveAccountDids } from "../libs/util"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { Settings } from "@/db/tables/settings"; +import { useClipboard } from "@vueuse/core"; @Component({ components: { LMap, @@ -186,6 +197,10 @@ export default class UserProfileView extends Vue { if (response.status === 200) { const result = await response.json(); this.profile = result.data; + if (this.profile && this.profile.rowId !== profileId) { + // currently the server returns "rowid" with lowercase "i"; remove when that's fixed + this.profile.rowId = profileId; + } } else { throw new Error("Failed to load profile"); } @@ -204,5 +219,23 @@ export default class UserProfileView extends Vue { this.isLoading = false; } } + + onCopyLinkClick() { + console.log("onCopyLinkClick", this.profile); + const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`; + useClipboard() + .copy(deepLink) + .then(() => { + this.$notify( + { + group: "alert", + type: "toast", + title: "Copied", + text: "A link to this profile was copied to the clipboard.", + }, + 2000, + ); + }); + } } -- 2.30.2 From 20ade415dc9e26eb0cedcc9e30168ad198fabe4e Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 16:31:31 -0600 Subject: [PATCH 3/7] bump to version 0.5.8 build 34 --- BUILDING.md | 5 +++-- CHANGELOG.md | 7 +++++++ android/app/build.gradle | 4 ++-- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index a159d900..b9167c1c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -41,6 +41,7 @@ Install dependencies: 1. Run the production build: ```bash + rm -rf dist npm run build:web ``` @@ -360,10 +361,10 @@ Prerequisites: macOS with Xcode installed ``` cd ios/App - xcrun agvtool new-version 33 + xcrun agvtool new-version 34 # Unfortunately this edits Info.plist directly. #xcrun agvtool new-marketing-version 0.4.5 - cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.7;/g" > temp && mv temp App.xcodeproj/project.pbxproj + cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj cd - ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 71657a57..b6ce5430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.8] +### Added +- /deep-link/ path for URLs that are shared with people +### Changed +- External links now go to /deep-link/... +- Feed visuals now have arrow imagery from giver to receiver + ## [0.4.7] ### Fixed diff --git a/android/app/build.gradle b/android/app/build.gradle index 810eccff..bfa25355 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "app.timesafari.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 33 - versionName "0.5.7" + versionCode 34 + versionName "0.5.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index e4a7b5e3..d3e6b9b4 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.7; + MARKETING_VERSION = 0.5.8; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -430,7 +430,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.7; + MARKETING_VERSION = 0.5.8; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package.json b/package.json index 935ece64..06722ca3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.8", "description": "Time Safari Application", "author": { "name": "Time Safari Team" -- 2.30.2 From c4a54967bc140430f46844edeb719cc6dcf9e353 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 16:33:55 -0600 Subject: [PATCH 4/7] fix linting --- src/views/ConfirmGiftView.vue | 4 ++-- src/views/ContactQRScanFullView.vue | 4 +++- src/views/ContactQRScanShowView.vue | 4 +++- src/views/UserProfileView.vue | 15 ++++++--------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 793df516..fbf81bab 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -679,8 +679,8 @@ export default class ConfirmGiftView extends Vue { /** * Add participant (giver/recipient) name & URL info */ - this.giverName = this.didInfo(this.giveDetails?.agentDid); - if (this.giveDetails?.agentDid) { + this.giverName = this.didInfo(this.giveDetails?.agentDid); + if (this.giveDetails?.agentDid) { this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`; } this.recipientName = this.didInfo(this.giveDetails?.recipientDid); diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index 1ca08edc..c5a08b74 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -593,7 +593,9 @@ export default class ContactQRScanFull extends Vue { } async onCopyUrlToClipboard() { - const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const account = (await libsUtil.retrieveFullyDecryptedAccount( + this.activeDid, + )) as Account; const jwtUrl = await generateEndorserJwtUrlForAccount( account, this.isRegistered, diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 675559e7..29b28e5f 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -672,7 +672,9 @@ export default class ContactQRScanShow extends Vue { } async onCopyUrlToClipboard() { - const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const account = (await libsUtil.retrieveFullyDecryptedAccount( + this.activeDid, + )) as Account; const jwtUrl = await generateEndorserJwtUrlForAccount( account, this.isRegistered, diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index 11db4a09..1defd348 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -16,9 +16,7 @@ Individual Profile -

- -
+
@@ -35,11 +33,11 @@
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }} -

@@ -221,7 +219,6 @@ export default class UserProfileView extends Vue { } onCopyLinkClick() { - console.log("onCopyLinkClick", this.profile); const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`; useClipboard() .copy(deepLink) -- 2.30.2 From 16557f1e4b55eecfaa314aefb8d5623f710cd68e Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 17:32:41 -0600 Subject: [PATCH 5/7] update build instruction & package-lock.json --- BUILDING.md | 4 +++- package-lock.json | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index b9167c1c..831d5d40 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -65,6 +65,8 @@ Install dependencies: * Commit everything (since the commit hash is used the app). +* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build` + * Put the commit hash in the changelog (which will help you remember to bump the version later). * Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`. @@ -72,7 +74,7 @@ Install dependencies: * For test, build the app (because test server is not yet set up to build): ```bash -TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build +TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web ``` ... and transfer to the test server: diff --git a/package-lock.json b/package-lock.json index a3e24731..7b2d9a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.8", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", -- 2.30.2 From 1e0efe601189d8f37dd8daf0530537e1a702800b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 18:32:55 -0600 Subject: [PATCH 6/7] lengthen the error timeout when the message may be complicated, eg. with details from the server --- src/views/ContactsView.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index faf63dbf..fbf85774 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -617,7 +617,7 @@ export default class ContactsView extends Vue { title: "Error with Invite", text: message, }, - 5000, + -1, ); } // if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter @@ -1122,7 +1122,7 @@ export default class ContactsView extends Vue { (regResult.error as string) || "Something went wrong during registration.", }, - 5000, + -1, ); } } catch (error) { @@ -1156,7 +1156,7 @@ export default class ContactsView extends Vue { title: "Registration Error", text: userMessage, }, - 5000, + -1, ); } } -- 2.30.2 From e9a8a3c1e7f6916b824a4642a8bd9d644c68537f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 19:31:16 -0600 Subject: [PATCH 7/7] add support for deep-link query parameters --- src/services/deepLinks.ts | 64 +++++++++++++++--------------- src/views/DeepLinkRedirectView.vue | 44 +++++++++++--------- 2 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 8c8aa501..bff3346c 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -115,7 +115,7 @@ export class DeepLinkHandler { const [path, queryString] = parts[1].split("?"); const [routePath, ...pathParams] = path.split("/"); - // logger.log( + // logger.info( // "[DeepLink] Debug:", // "Route Path:", // routePath, @@ -150,37 +150,6 @@ export class DeepLinkHandler { 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 { - try { - logConsoleAndDb("[DeepLink] Processing URL: " + url, false); - const { path, params, query } = this.parseDeepLink(url); - // Ensure params is always a Record 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. @@ -256,4 +225,35 @@ export class DeepLinkHandler { }; } } + + /** + * 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 { + try { + logConsoleAndDb("[DeepLink] Processing URL: " + url, false); + const { path, params, query } = this.parseDeepLink(url); + // Ensure params is always a Record 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, + }; + } + } } diff --git a/src/views/DeepLinkRedirectView.vue b/src/views/DeepLinkRedirectView.vue index 1615cfd4..74ea25d7 100644 --- a/src/views/DeepLinkRedirectView.vue +++ b/src/views/DeepLinkRedirectView.vue @@ -122,17 +122,31 @@ export default class DeepLinkRedirectView extends Vue { // If pathParam is an array (catch-all parameter), join it const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam; - this.destinationUrl = fullPath; - this.deepLinkUrl = `timesafari://${fullPath}`; - this.webUrl = `${APP_SERVER}/${fullPath}`; - - // Log for debugging - logger.info("Deep link processing:", { - fullPath, - deepLinkUrl: this.deepLinkUrl, - webUrl: this.webUrl, - userAgent: this.userAgent, - }); + + // Get query parameters from the route + const queryParams = this.$route.query; + + // Build query string if there are query parameters + let queryString = ""; + if (Object.keys(queryParams).length > 0) { + const searchParams = new URLSearchParams(); + Object.entries(queryParams).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + const stringValue = Array.isArray(value) ? value[0] : value; + if (stringValue !== null && stringValue !== undefined) { + searchParams.append(key, stringValue); + } + } + }); + queryString = "?" + searchParams.toString(); + } + + // Combine path with query parameters + const fullPathWithQuery = fullPath + queryString; + + this.destinationUrl = fullPathWithQuery; + this.deepLinkUrl = `timesafari://${fullPathWithQuery}`; + this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`; this.isDevelopment = process.env.NODE_ENV !== "production"; this.userAgent = navigator.userAgent; @@ -147,13 +161,6 @@ export default class DeepLinkRedirectView extends Vue { return; } - logger.info("Attempting deep link redirect:", { - deepLinkUrl: this.deepLinkUrl, - webUrl: this.webUrl, - isMobile: this.isMobile, - userAgent: this.userAgent, - }); - try { // For mobile, try the deep link URL; for desktop, use the web URL const redirectUrl = this.isMobile ? this.deepLinkUrl : this.webUrl; @@ -170,7 +177,6 @@ export default class DeepLinkRedirectView extends Vue { document.body.appendChild(link); link.click(); document.body.removeChild(link); - logger.info("Fallback link click completed"); } catch (error) { logger.error( "Fallback deep link failed: " + errorStringForLog(error), -- 2.30.2