forked from jsnbuchanan/crowd-funder-for-time-pwa
fix: invite-one-accept deep link would not route properly
This commit is contained in:
@@ -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(),
|
||||||
|
|||||||
@@ -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");
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.router.replace({
|
|
||||||
name: routeName,
|
|
||||||
params: validatedParams,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// For parameter validation errors, provide specific error feedback
|
// 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({
|
await this.router.replace({
|
||||||
name: "deep-link-error",
|
name: "deep-link-error",
|
||||||
|
params,
|
||||||
query: {
|
query: {
|
||||||
originalPath: path,
|
originalPath: path,
|
||||||
errorCode: "INVALID_PARAMETERS",
|
errorCode: "INVALID_PARAMETERS",
|
||||||
message: `The link parameters are invalid: ${(error as Error).message}`,
|
errorMessage: `The link parameters are invalid: ${(error as Error).message}`,
|
||||||
|
...query,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
throw {
|
// This previously threw an error but we're redirecting so there's no need.
|
||||||
code: "INVALID_PARAMETERS",
|
return;
|
||||||
message: (error as Error).message,
|
|
||||||
details: error,
|
|
||||||
params: params,
|
|
||||||
query: query,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.router.replace({
|
||||||
|
name: routeName,
|
||||||
|
params: validatedParams,
|
||||||
|
query: validatedQuery,
|
||||||
|
});
|
||||||
|
} 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
|
||||||
|
await this.router.replace({
|
||||||
|
name: "deep-link-error",
|
||||||
|
params: validatedParams,
|
||||||
|
query: {
|
||||||
|
originalPath: path,
|
||||||
|
errorCode: "ROUTING_ERROR",
|
||||||
|
errorMessage: `Error routing to ${routeName}: ${(JSON.stringify(error))}`,
|
||||||
|
...validatedQuery,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user