forked from jsnbuchanan/crowd-funder-for-time-pwa
Fix InviteOneAcceptView migration fence; remove USE_DEXIE_DB dependency
- Remove USE_DEXIE_DB import from app.ts constants - Update InviteOneAcceptView to use PlatformServiceMixin pattern - Remove legacy Dexie database access code - Move WORKER_ONLY_DATABASE_IMPLEMENTATION.md to doc/ directory - Remerge changes in router/index.ts Fixes Electron build failure caused by missing USE_DEXIE_DB export.
This commit is contained in:
@@ -27,37 +27,8 @@
|
|||||||
*/
|
*/
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
// Add a union type of all valid route paths
|
|
||||||
export const VALID_DEEP_LINK_ROUTES = [
|
|
||||||
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
|
||||||
"claim",
|
|
||||||
"claim-add-raw",
|
|
||||||
"claim-cert",
|
|
||||||
"confirm-gift",
|
|
||||||
"contact-import",
|
|
||||||
"did",
|
|
||||||
"invite-one-accept",
|
|
||||||
"onboard-meeting-setup",
|
|
||||||
"project",
|
|
||||||
"user-profile",
|
|
||||||
] 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
|
// Parameter validation schemas for each route type
|
||||||
export const deepLinkSchemas = {
|
export const deepLinkSchemas = {
|
||||||
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
|
||||||
claim: z.object({
|
claim: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
@@ -72,16 +43,24 @@ export const deepLinkSchemas = {
|
|||||||
"confirm-gift": z.object({
|
"confirm-gift": z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
|
"contact-edit": z.object({
|
||||||
|
did: z.string(),
|
||||||
|
}),
|
||||||
"contact-import": z.object({
|
"contact-import": z.object({
|
||||||
jwt: z.string(),
|
jwt: z.string(),
|
||||||
}),
|
}),
|
||||||
|
contacts: z.object({
|
||||||
|
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-setup": z.object({
|
"onboard-meeting-members": z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
project: z.object({
|
project: z.object({
|
||||||
@@ -92,6 +71,19 @@ export const deepLinkSchemas = {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a union type of all valid route paths
|
||||||
|
export const VALID_DEEP_LINK_ROUTES = Object.keys(deepLinkSchemas) as readonly (keyof typeof deepLinkSchemas)[];
|
||||||
|
|
||||||
export type DeepLinkParams = {
|
export type DeepLinkParams = {
|
||||||
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
||||||
};
|
};
|
||||||
@@ -100,3 +92,6 @@ export interface DeepLinkError extends Error {
|
|||||||
code: string;
|
code: string;
|
||||||
details?: unknown;
|
details?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the type to ensure route validation
|
||||||
|
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES as [string, ...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",
|
||||||
|
|||||||
@@ -44,15 +44,41 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deepLinkSchemas,
|
deepLinkSchemas,
|
||||||
baseUrlSchema,
|
baseUrlSchema,
|
||||||
routeSchema,
|
routeSchema,
|
||||||
DeepLinkRoute,
|
DeepLinkRoute,
|
||||||
} from "../interfaces/deepLinks";
|
} from "../interfaces/deepLinks";
|
||||||
// Legacy databaseUtil import removed - using logger instead
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
|
// Helper function to extract the first key from a Zod object schema
|
||||||
|
function getFirstKeyFromZodObject(schema: z.ZodObject<any>): string | undefined {
|
||||||
|
const shape = schema.shape;
|
||||||
|
const keys = Object.keys(shape);
|
||||||
|
return keys.length > 0 ? keys[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps deep link routes to their corresponding Vue router names and optional parameter keys.
|
||||||
|
*
|
||||||
|
* It's an object where keys are the deep link routes and values are objects with 'name' and 'paramKey'.
|
||||||
|
*
|
||||||
|
* The paramKey is used to extract the parameter from the route path,
|
||||||
|
* because "router.replace" expects the right parameter name for the route.
|
||||||
|
*/
|
||||||
|
export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> =
|
||||||
|
Object.entries(deepLinkSchemas).reduce((acc, [routeName, schema]) => {
|
||||||
|
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
|
||||||
|
acc[routeName] = {
|
||||||
|
name: routeName,
|
||||||
|
paramKey
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, { name: string; paramKey?: string }>);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles processing and routing of deep links in the application.
|
* Handles processing and routing of deep links in the application.
|
||||||
@@ -70,30 +96,7 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps deep link routes to their corresponding Vue router names and optional parameter keys.
|
|
||||||
*
|
|
||||||
* The paramKey is used to extract the parameter from the route path,
|
|
||||||
* because "router.replace" expects the right parameter name for the route.
|
|
||||||
* The default is "id".
|
|
||||||
*/
|
|
||||||
private readonly ROUTE_MAP: Record<
|
|
||||||
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" },
|
|
||||||
project: { name: "project" },
|
|
||||||
"user-profile": { name: "user-profile" },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses deep link URL into path, params and query components.
|
* Parses deep link URL into path, params and query components.
|
||||||
* Validates URL structure using Zod schemas.
|
* Validates URL structure using Zod schemas.
|
||||||
*
|
*
|
||||||
@@ -116,18 +119,9 @@ 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 (!this.ROUTE_MAP[routePath]) {
|
if (!ROUTE_MAP[routePath]) {
|
||||||
throw {
|
throw {
|
||||||
code: "INVALID_ROUTE",
|
code: "INVALID_ROUTE",
|
||||||
message: `Invalid route path: ${routePath}`,
|
message: `Invalid route path: ${routePath}`,
|
||||||
@@ -145,9 +139,14 @@ export class DeepLinkHandler {
|
|||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (pathParams) {
|
if (pathParams) {
|
||||||
// Now we know routePath exists in ROUTE_MAP
|
// Now we know routePath exists in ROUTE_MAP
|
||||||
const routeConfig = this.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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,60 +170,73 @@ export class DeepLinkHandler {
|
|||||||
try {
|
try {
|
||||||
// Validate route exists
|
// Validate route exists
|
||||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
||||||
routeName = this.ROUTE_MAP[validRoute].name;
|
routeName = ROUTE_MAP[validRoute].name;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the invalid route attempt
|
// Log the invalid route attempt
|
||||||
logger.error(`[DeepLink] Invalid route path: ${path}`);
|
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
|
||||||
|
|
||||||
// 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,7 +248,6 @@ export class DeepLinkHandler {
|
|||||||
*/
|
*/
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
async handleDeepLink(url: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info("[DeepLink] Processing URL: " + url);
|
|
||||||
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(
|
||||||
@@ -245,8 +256,9 @@ export class DeepLinkHandler {
|
|||||||
await this.validateAndRoute(path, sanitizedParams, query);
|
await this.validateAndRoute(path, sanitizedParams, query);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const deepLinkError = error as DeepLinkError;
|
const deepLinkError = error as DeepLinkError;
|
||||||
logger.error(
|
logConsoleAndDb(
|
||||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
throw {
|
throw {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
<code>timesafari://{{ formattedPath }}</code>
|
<code>timesafari://{{ formattedPath }}</code>
|
||||||
<div class="debug-info">
|
<div class="debug-info">
|
||||||
<h4>Parameters:</h4>
|
<h4>Parameters:</h4>
|
||||||
<pre>{{ JSON.stringify($route.params, null, 2) }}</pre>
|
<pre>{{ JSON.stringify(route.params, null, 2) }}</pre>
|
||||||
<h4>Query:</h4>
|
<h4>Query:</h4>
|
||||||
<pre>{{ JSON.stringify($route.query, null, 2) }}</pre>
|
<pre>{{ JSON.stringify(route.query, null, 2) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,104 +31,79 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Testing Required - Vue 3 to Options API + PlatformServiceMixin Migration
|
import { computed, onMounted } from "vue";
|
||||||
// Priority: Medium | Migrated: 2025-07-06 | Author: Matthew Raymer
|
import { useRoute, useRouter } from "vue-router";
|
||||||
//
|
import { VALID_DEEP_LINK_ROUTES, deepLinkSchemas } from "../interfaces/deepLinks";
|
||||||
// MIGRATION DETAILS: Converted from Vue 3 Composition API to Options API for PlatformServiceMixin
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
// - Replaced logConsoleAndDb() with this.$logAndConsole()
|
|
||||||
// - Converted computed properties to getters
|
|
||||||
// - Converted onMounted to mounted() lifecycle hook
|
|
||||||
//
|
|
||||||
// TESTING NEEDED:
|
|
||||||
// 1. Test invalid deep link triggering (easy to test)
|
|
||||||
// 2. Verify error logging works correctly
|
|
||||||
// 3. Test error page display and navigation
|
|
||||||
// 4. Test "Report Issue" functionality
|
|
||||||
//
|
|
||||||
// Test URL: timesafari://invalid/path?param=test
|
|
||||||
|
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|
||||||
import { VALID_DEEP_LINK_ROUTES } from "../interfaces/deepLinks";
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
|
||||||
|
|
||||||
@Component({
|
const route = useRoute();
|
||||||
mixins: [PlatformServiceMixin],
|
const router = useRouter();
|
||||||
})
|
// an object with the route as the key and the first param name as the value
|
||||||
export default class DeepLinkErrorView extends Vue {
|
const deepLinkSchemaKeys = Object.fromEntries(
|
||||||
/** Current route instance */
|
Object.entries(deepLinkSchemas).map(([route, schema]) => {
|
||||||
$route!: RouteLocationNormalizedLoaded;
|
const param = Object.keys(schema.shape)[0];
|
||||||
/** Router instance for navigation */
|
return [route, param];
|
||||||
$router!: Router;
|
})
|
||||||
|
);
|
||||||
|
|
||||||
validRoutes = VALID_DEEP_LINK_ROUTES;
|
// Extract error information from query params
|
||||||
|
const errorCode = computed(
|
||||||
|
() => (route.query.errorCode as string) || "UNKNOWN_ERROR",
|
||||||
|
);
|
||||||
|
const errorMessage = computed(
|
||||||
|
() =>
|
||||||
|
(route.query.errorMessage as string) ||
|
||||||
|
"The deep link you followed is invalid or not supported.",
|
||||||
|
);
|
||||||
|
const originalPath = computed(() => route.query.originalPath as string);
|
||||||
|
const validRoutes = VALID_DEEP_LINK_ROUTES;
|
||||||
|
|
||||||
// Extract error information from query params
|
// Format the path and include any parameters
|
||||||
get errorCode(): string {
|
const formattedPath = computed(() => {
|
||||||
return (this.$route.query.errorCode as string) || "UNKNOWN_ERROR";
|
if (!originalPath.value) return "";
|
||||||
}
|
const path = originalPath.value.replace(/^\/+/, "");
|
||||||
|
|
||||||
get errorMessage(): string {
|
// Log for debugging
|
||||||
return (
|
logger.log(
|
||||||
(this.$route.query.message as string) ||
|
"[DeepLinkError] Original Path:",
|
||||||
"The deep link you followed is invalid or not supported."
|
originalPath.value,
|
||||||
);
|
"Route Params:",
|
||||||
}
|
route.params,
|
||||||
|
"Route Query:",
|
||||||
|
route.query,
|
||||||
|
);
|
||||||
|
|
||||||
get originalPath(): string {
|
return path;
|
||||||
return this.$route.query.originalPath as string;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Format the path and include any parameters
|
// Navigation methods
|
||||||
get formattedPath(): string {
|
const goHome = () => router.replace({ name: "home" });
|
||||||
if (!this.originalPath) return "";
|
const reportIssue = () => {
|
||||||
const path = this.originalPath.replace(/^\/+/, "");
|
// Open a support form or email
|
||||||
|
window.open(
|
||||||
|
"mailto:support@timesafari.app?subject=Invalid Deep Link&body=" +
|
||||||
|
encodeURIComponent(
|
||||||
|
`I encountered an error with a deep link: timesafari://${originalPath.value}\nError: ${errorMessage.value}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Log for debugging
|
// Log the error for analytics
|
||||||
logger.log(
|
onMounted(() => {
|
||||||
"[DeepLinkError] Original Path:",
|
logConsoleAndDb(
|
||||||
this.originalPath,
|
`[DeepLinkError] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`,
|
||||||
"Route Params:",
|
true,
|
||||||
this.$route.params,
|
);
|
||||||
"Route Query:",
|
});
|
||||||
this.$route.query,
|
|
||||||
);
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation methods
|
|
||||||
goHome(): void {
|
|
||||||
this.$router.replace({ name: "home" });
|
|
||||||
}
|
|
||||||
|
|
||||||
reportIssue(): void {
|
|
||||||
// Open a support form or email
|
|
||||||
window.open(
|
|
||||||
"mailto:support@timesafari.app?subject=Invalid Deep Link&body=" +
|
|
||||||
encodeURIComponent(
|
|
||||||
`I encountered an error with a deep link: timesafari://${this.originalPath}\nError: ${this.errorMessage}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log the error for analytics
|
|
||||||
async mounted(): Promise<void> {
|
|
||||||
this.$logAndConsole(
|
|
||||||
`[DeepLink] Error page displayed for path: ${this.originalPath}, code: ${this.errorCode}, params: ${JSON.stringify(this.$route.params)}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -24,12 +24,12 @@
|
|||||||
placeholder="Paste invitation..."
|
placeholder="Paste invitation..."
|
||||||
class="mt-4 border-2 border-gray-300 p-2 rounded"
|
class="mt-4 border-2 border-gray-300 p-2 rounded"
|
||||||
cols="30"
|
cols="30"
|
||||||
@input="handleInputChange"
|
@input="() => checkInvite(inputJwt)"
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<button
|
<button
|
||||||
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
class="ml-2 p-2 bg-blue-500 text-white rounded"
|
||||||
@click="handleAcceptClick"
|
@click="() => processInvite(inputJwt, true)"
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
</button>
|
</button>
|
||||||
@@ -43,26 +43,13 @@ import { Router, RouteLocationNormalized } from "vue-router";
|
|||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { logConsoleAndDb } from "../db/index";
|
import {
|
||||||
|
logConsoleAndDb,
|
||||||
|
} from "../db/index";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
import { decodeEndorserJwt } from "../libs/crypto/vc";
|
||||||
import { errorStringForLog } from "../libs/endorserServer";
|
import { errorStringForLog } from "../libs/endorserServer";
|
||||||
import { generateSaveAndActivateIdentity } from "../libs/util";
|
import { generateSaveAndActivateIdentity } from "../libs/util";
|
||||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
import {
|
|
||||||
NOTIFY_INVITE_MISSING,
|
|
||||||
NOTIFY_INVITE_PROCESSING_ERROR,
|
|
||||||
NOTIFY_INVITE_TRUNCATED_DATA,
|
|
||||||
} from "../constants/notifications";
|
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @file InviteOneAcceptView.vue
|
|
||||||
* @description Invitation acceptance flow for single-use invitations to join the platform.
|
|
||||||
* Processes JWTs from various sources (URL, text input) and redirects to contacts page
|
|
||||||
* for completion of the invitation process.
|
|
||||||
* @author Matthew Raymer
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invite One Accept View Component
|
* Invite One Accept View Component
|
||||||
@@ -91,7 +78,6 @@ import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
components: { QuickNav },
|
components: { QuickNav },
|
||||||
mixins: [PlatformServiceMixin],
|
|
||||||
})
|
})
|
||||||
export default class InviteOneAcceptView extends Vue {
|
export default class InviteOneAcceptView extends Vue {
|
||||||
/** Notification function injected by Vue */
|
/** Notification function injected by Vue */
|
||||||
@@ -101,9 +87,6 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
/** Route instance for current route */
|
/** Route instance for current route */
|
||||||
$route!: RouteLocationNormalized;
|
$route!: RouteLocationNormalized;
|
||||||
|
|
||||||
// Notification helper system
|
|
||||||
private notify = createNotifyHelpers(this.$notify);
|
|
||||||
|
|
||||||
/** Active user's DID */
|
/** Active user's DID */
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
/** API server endpoint */
|
/** API server endpoint */
|
||||||
@@ -117,7 +100,7 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
* Component lifecycle hook that initializes invite processing
|
* Component lifecycle hook that initializes invite processing
|
||||||
*
|
*
|
||||||
* Workflow:
|
* Workflow:
|
||||||
* 1. Loads account settings using PlatformServiceMixin
|
* 1. Opens database connection
|
||||||
* 2. Retrieves account settings
|
* 2. Retrieves account settings
|
||||||
* 3. Ensures active DID exists or generates one
|
* 3. Ensures active DID exists or generates one
|
||||||
* 4. Extracts JWT from URL path
|
* 4. Extracts JWT from URL path
|
||||||
@@ -129,44 +112,20 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
async mounted() {
|
async mounted() {
|
||||||
this.checkingInvite = true;
|
this.checkingInvite = true;
|
||||||
|
|
||||||
try {
|
// Load or generate identity
|
||||||
logger.debug(
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
"[InviteOneAcceptView] Component mounted - processing invitation",
|
this.activeDid = settings.activeDid || "";
|
||||||
);
|
this.apiServer = settings.apiServer || "";
|
||||||
|
|
||||||
// Load or generate identity using PlatformServiceMixin
|
if (!this.activeDid) {
|
||||||
const settings = await this.$accountSettings();
|
this.activeDid = await generateSaveAndActivateIdentity();
|
||||||
this.activeDid = settings.activeDid || "";
|
|
||||||
this.apiServer = settings.apiServer || "";
|
|
||||||
|
|
||||||
logger.debug("[InviteOneAcceptView] Account settings loaded", {
|
|
||||||
hasActiveDid: !!this.activeDid,
|
|
||||||
hasApiServer: !!this.apiServer,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.activeDid) {
|
|
||||||
logger.debug(
|
|
||||||
"[InviteOneAcceptView] No active DID found, generating new identity",
|
|
||||||
);
|
|
||||||
this.activeDid = await generateSaveAndActivateIdentity();
|
|
||||||
logger.debug("[InviteOneAcceptView] New identity generated", {
|
|
||||||
newActiveDid: !!this.activeDid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract JWT from route path
|
|
||||||
const jwt = (this.$route.params.jwt as string) || "";
|
|
||||||
logger.debug("[InviteOneAcceptView] Processing invite from route", {
|
|
||||||
hasJwt: !!jwt,
|
|
||||||
jwtLength: jwt.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.processInvite(jwt, false);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[InviteOneAcceptView] Error during mount:", error);
|
|
||||||
} finally {
|
|
||||||
this.checkingInvite = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract JWT from route path
|
||||||
|
const jwt = (this.$route.params.jwt as string) || this.$route.query.jwt as string || "";
|
||||||
|
await this.processInvite(jwt, false);
|
||||||
|
|
||||||
|
this.checkingInvite = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,7 +224,15 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
*/
|
*/
|
||||||
private handleMissingJwt(notify: boolean) {
|
private handleMissingJwt(notify: boolean) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
this.notify.error(NOTIFY_INVITE_MISSING.message, TIMEOUTS.LONG);
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Missing Invite",
|
||||||
|
text: "There was no invite. Paste the entire text that has the data.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +246,15 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
logConsoleAndDb(fullError, true);
|
logConsoleAndDb(fullError, true);
|
||||||
|
|
||||||
if (notify) {
|
if (notify) {
|
||||||
this.notify.error(NOTIFY_INVITE_PROCESSING_ERROR.message, TIMEOUTS.BRIEF);
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "There was an error processing that invite.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,35 +277,16 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
jwtInput.endsWith("invite-one-accept") ||
|
jwtInput.endsWith("invite-one-accept") ||
|
||||||
jwtInput.endsWith("invite-one-accept/")
|
jwtInput.endsWith("invite-one-accept/")
|
||||||
) {
|
) {
|
||||||
this.notify.error(NOTIFY_INVITE_TRUNCATED_DATA.message, TIMEOUTS.LONG);
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "That is only part of the invite data; it's missing some at the end. Try another way to get the full data.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Template handler for input change events
|
|
||||||
*
|
|
||||||
* Called when user types in the invitation text input field.
|
|
||||||
* Validates the input for common error patterns.
|
|
||||||
*
|
|
||||||
* @throws Will not throw but shows notifications
|
|
||||||
* @emits Notifications on validation errors
|
|
||||||
*/
|
|
||||||
handleInputChange() {
|
|
||||||
this.checkInvite(this.inputJwt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Template handler for Accept button click
|
|
||||||
*
|
|
||||||
* Processes the invitation with user notification enabled.
|
|
||||||
* This is the explicit user action to accept an invitation.
|
|
||||||
*
|
|
||||||
* @throws Will not throw but logs errors
|
|
||||||
* @emits Notifications on errors
|
|
||||||
* @emits Router navigation on success
|
|
||||||
*/
|
|
||||||
handleAcceptClick() {
|
|
||||||
this.processInvite(this.inputJwt, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user