Browse Source

fix: invite-one-accept deep link would not route properly

Trent Larson 1 week ago
parent
commit
33ce6bdb72
  1. 6
      src/interfaces/deepLinks.ts
  2. 11
      src/main.capacitor.ts
  3. 13
      src/router/index.ts
  4. 68
      src/services/deepLinks.ts
  5. 15
      src/views/DeepLinkErrorView.vue
  6. 2
      src/views/InviteOneAcceptView.vue

6
src/interfaces/deepLinks.ts

@ -50,13 +50,15 @@ export const deepLinkSchemas = {
jwt: z.string(), jwt: z.string(),
}), }),
contacts: z.object({ contacts: z.object({
contacts: z.string(), // JSON string of contacts array contactJwt: z.string().optional(),
inviteJwt: z.string().optional(),
}), }),
did: z.object({ did: z.object({
did: z.string(), did: z.string(),
}), }),
"invite-one-accept": z.object({ "invite-one-accept": z.object({
jwt: z.string(), // optional because A) it could be a query param, and B) the page displays an input if things go wrong
jwt: z.string().optional(),
}), }),
"onboard-meeting-members": z.object({ "onboard-meeting-members": z.object({
id: z.string(), id: z.string(),

11
src/main.capacitor.ts

@ -72,12 +72,11 @@ const handleDeepLink = async (data: { url: string }) => {
await deepLinkHandler.handleDeepLink(data.url); await deepLinkHandler.handleDeepLink(data.url);
} catch (error) { } catch (error) {
logger.error("[DeepLink] Error handling deep link: ", error); logger.error("[DeepLink] Error handling deep link: ", error);
handleApiError( let message: string = error instanceof Error ? error.message : safeStringify(error);
{ if (data.url) {
message: error instanceof Error ? error.message : safeStringify(error), message += `\nURL: ${data.url}`;
} as AxiosError, }
"deep-link", handleApiError({ message } as AxiosError, "deep-link");
);
} }
}; };

13
src/router/index.ts

@ -73,6 +73,11 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts", name: "contacts",
component: () => import("../views/ContactsView.vue"), component: () => import("../views/ContactsView.vue"),
}, },
{
path: "/database-migration",
name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"),
},
{ {
path: "/did/:did?", path: "/did/:did?",
name: "did", name: "did",
@ -139,8 +144,9 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("../views/InviteOneView.vue"), component: () => import("../views/InviteOneView.vue"),
}, },
{ {
// optional because A) it could be a query param, and B) the page displays an input if things go wrong
path: "/invite-one-accept/:jwt?", path: "/invite-one-accept/:jwt?",
name: "InviteOneAcceptView", name: "invite-one-accept",
component: () => import("../views/InviteOneAcceptView.vue"), component: () => import("../views/InviteOneAcceptView.vue"),
}, },
{ {
@ -148,11 +154,6 @@ const routes: Array<RouteRecordRaw> = [
name: "logs", name: "logs",
component: () => import("../views/LogView.vue"), component: () => import("../views/LogView.vue"),
}, },
{
path: "/database-migration",
name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"),
},
{ {
path: "/new-activity", path: "/new-activity",
name: "new-activity", name: "new-activity",

68
src/services/deepLinks.ts

@ -119,15 +119,6 @@ export class DeepLinkHandler {
const [path, queryString] = parts[1].split("?"); const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/"); const [routePath, ...pathParams] = path.split("/");
// logger.info(
// "[DeepLink] Debug:",
// "Route Path:",
// routePath,
// "Path Params:",
// pathParams,
// "Query String:",
// queryString,
// );
// Validate route exists before proceeding // Validate route exists before proceeding
if (!ROUTE_MAP[routePath]) { if (!ROUTE_MAP[routePath]) {
@ -151,6 +142,11 @@ export class DeepLinkHandler {
const routeConfig = ROUTE_MAP[routePath]; const routeConfig = ROUTE_MAP[routePath];
params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
} }
// logConsoleAndDb(
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`,
// false,
// );
return { path: routePath, params, query }; return { path: routePath, params, query };
} }
@ -182,52 +178,65 @@ export class DeepLinkHandler {
// Redirect to error page with information about the invalid link // Redirect to error page with information about the invalid link
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "INVALID_ROUTE", errorCode: "INVALID_ROUTE",
message: `The link you followed (${path}) is not supported`, errorMessage: `The link you followed (${path}) is not supported`,
...query,
}, },
}); });
throw { // This previously threw an error but we're redirecting so there's no need.
code: "INVALID_ROUTE", return;
message: `Unsupported route: ${path}`,
};
} }
// Continue with parameter validation as before... // Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
let validatedParams, validatedQuery;
try { try {
const validatedParams = await schema.parseAsync({ validatedParams = await schema.parseAsync(params);
...params, validatedQuery = await schema.parseAsync(query);
...query, } catch (error) {
// For parameter validation errors, provide specific error feedback
logConsoleAndDb(`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, true);
await this.router.replace({
name: "deep-link-error",
params,
query: {
originalPath: path,
errorCode: "INVALID_PARAMETERS",
errorMessage: `The link parameters are invalid: ${(error as Error).message}`,
...query,
},
}); });
// This previously threw an error but we're redirecting so there's no need.
return;
}
try {
await this.router.replace({ await this.router.replace({
name: routeName, name: routeName,
params: validatedParams, params: validatedParams,
query, query: validatedQuery,
}); });
} catch (error) { } catch (error) {
logConsoleAndDb(`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)} ... and validated query: ${JSON.stringify(validatedQuery)}`, true);
// For parameter validation errors, provide specific error feedback // For parameter validation errors, provide specific error feedback
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params: validatedParams,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "INVALID_PARAMETERS", errorCode: "ROUTING_ERROR",
message: `The link parameters are invalid: ${(error as Error).message}`, errorMessage: `Error routing to ${routeName}: ${(JSON.stringify(error))}`,
...validatedQuery,
}, },
}); });
throw {
code: "INVALID_PARAMETERS",
message: (error as Error).message,
details: error,
params: params,
query: query,
};
} }
} }
/** /**
@ -239,7 +248,6 @@ export class DeepLinkHandler {
*/ */
async handleDeepLink(url: string): Promise<void> { async handleDeepLink(url: string): Promise<void> {
try { try {
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
const { path, params, query } = this.parseDeepLink(url); const { path, params, query } = this.parseDeepLink(url);
// Ensure params is always a Record<string,string> by converting undefined to empty string // Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries( const sanitizedParams = Object.fromEntries(
@ -249,7 +257,7 @@ export class DeepLinkHandler {
} catch (error) { } catch (error) {
const deepLinkError = error as DeepLinkError; const deepLinkError = error as DeepLinkError;
logConsoleAndDb( logConsoleAndDb(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`, `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
true, true,
); );

15
src/views/DeepLinkErrorView.vue

@ -31,7 +31,7 @@
<h2>Supported Deep Links</h2> <h2>Supported Deep Links</h2>
<ul> <ul>
<li v-for="(routeItem, index) in validRoutes" :key="index"> <li v-for="(routeItem, index) in validRoutes" :key="index">
<code>timesafari://{{ routeItem }}/:id</code> <code>timesafari://{{ routeItem }}/:{{ deepLinkSchemaKeys[routeItem] }}</code>
</li> </li>
</ul> </ul>
</div> </div>
@ -41,12 +41,19 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { VALID_DEEP_LINK_ROUTES } from "../interfaces/deepLinks"; import { VALID_DEEP_LINK_ROUTES, deepLinkSchemas } from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil"; import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
// an object with the route as the key and the first param name as the value
const deepLinkSchemaKeys = Object.fromEntries(
Object.entries(deepLinkSchemas).map(([route, schema]) => {
const param = Object.keys(schema.shape)[0];
return [route, param];
})
);
// Extract error information from query params // Extract error information from query params
const errorCode = computed( const errorCode = computed(
@ -54,7 +61,7 @@ const errorCode = computed(
); );
const errorMessage = computed( const errorMessage = computed(
() => () =>
(route.query.message as string) || (route.query.errorMessage as string) ||
"The deep link you followed is invalid or not supported.", "The deep link you followed is invalid or not supported.",
); );
const originalPath = computed(() => route.query.originalPath as string); const originalPath = computed(() => route.query.originalPath as string);
@ -93,7 +100,7 @@ const reportIssue = () => {
// Log the error for analytics // Log the error for analytics
onMounted(() => { onMounted(() => {
logConsoleAndDb( logConsoleAndDb(
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`, `[DeepLinkError] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`,
true, true,
); );
}); });

2
src/views/InviteOneAcceptView.vue

@ -128,7 +128,7 @@ export default class InviteOneAcceptView extends Vue {
} }
// Extract JWT from route path // Extract JWT from route path
const jwt = (this.$route.params.jwt as string) || ""; const jwt = (this.$route.params.jwt as string) || this.$route.query.jwt as string || "";
await this.processInvite(jwt, false); await this.processInvite(jwt, false);
this.checkingInvite = false; this.checkingInvite = false;

Loading…
Cancel
Save