Merge branch 'master' into profile_include_location

This commit is contained in:
Matthew Raymer
2025-08-20 09:48:26 +00:00
51 changed files with 4684 additions and 2000 deletions

View File

@@ -101,6 +101,7 @@ import {
import { Router } from "vue-router";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { logger } from "@/utils/logger";
@Component({
components: {
@@ -119,11 +120,13 @@ export default class FeedFilters extends Vue {
isNearby = false;
settingChanged = false;
visible = false;
activeDid = "";
async open(onCloseIfChanged: () => void) {
async open(onCloseIfChanged: () => void, activeDid: string) {
this.onCloseIfChanged = onCloseIfChanged;
this.activeDid = activeDid;
const settings = await this.$settings();
const settings = await this.$accountSettings(activeDid);
this.hasVisibleDid = !!settings.filterFeedByVisible;
this.isNearby = !!settings.filterFeedByNearby;
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
@@ -137,6 +140,7 @@ export default class FeedFilters extends Vue {
async toggleHasVisibleDid() {
this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid;
await this.$updateSettings({
filterFeedByVisible: this.hasVisibleDid,
});
@@ -145,9 +149,18 @@ export default class FeedFilters extends Vue {
async toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
logger.debug("[FeedFilters] 🔄 Toggling nearby filter:", {
newValue: this.isNearby,
settingChanged: this.settingChanged,
activeDid: this.activeDid,
});
await this.$updateSettings({
filterFeedByNearby: this.isNearby,
});
logger.debug("[FeedFilters] ✅ Nearby filter updated in settings");
}
async clearAll() {
@@ -179,13 +192,20 @@ export default class FeedFilters extends Vue {
}
close() {
logger.debug("[FeedFilters] 🚪 Closing dialog:", {
settingChanged: this.settingChanged,
hasCallback: !!this.onCloseIfChanged,
});
if (this.settingChanged) {
logger.debug("[FeedFilters] 🔄 Settings changed, calling callback");
this.onCloseIfChanged();
}
this.visible = false;
}
done() {
logger.debug("[FeedFilters] ✅ Done button clicked");
this.close();
}
}

View File

@@ -60,9 +60,13 @@ export interface AxiosErrorResponse {
[key: string]: unknown;
};
status?: number;
statusText?: string;
config?: unknown;
};
config?: unknown;
config?: {
url?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}

View File

@@ -28,7 +28,7 @@
import { z } from "zod";
// Parameter validation schemas for each route type
export const deepLinkSchemas = {
export const deepLinkPathSchemas = {
claim: z.object({
id: z.string(),
}),
@@ -60,7 +60,7 @@ export const deepLinkSchemas = {
jwt: z.string().optional(),
}),
"onboard-meeting-members": z.object({
id: z.string(),
groupId: z.string(),
}),
project: z.object({
id: z.string(),
@@ -70,6 +70,17 @@ export const deepLinkSchemas = {
}),
};
export const deepLinkQuerySchemas = {
"onboard-meeting-members": z.object({
password: z.string(),
}),
};
// Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = Object.keys(
deepLinkPathSchemas,
) as readonly (keyof typeof deepLinkPathSchemas)[];
// Create a type from the array
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
@@ -80,14 +91,13 @@ export const baseUrlSchema = z.object({
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 DeepLinkPathParams = {
// [K in keyof typeof deepLinkPathSchemas]: z.infer<(typeof deepLinkPathSchemas)[K]>;
// };
export type DeepLinkParams = {
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
};
// export type DeepLinkQueryParams = {
// [K in keyof typeof deepLinkQuerySchemas]: z.infer<(typeof deepLinkQuerySchemas)[K]>;
// };
export interface DeepLinkError extends Error {
code: string;

View File

@@ -29,14 +29,14 @@
*/
import { initializeApp } from "./main.common";
import { App } from "./libs/capacitor/app";
import { App as CapacitorApp } from "@capacitor/app";
import router from "./router";
import { handleApiError } from "./services/api";
import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks";
import { logger, safeStringify } from "./utils/logger";
logger.log("[Capacitor] Starting initialization");
logger.log("[Capacitor] 🚀 Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
const app = initializeApp();
@@ -67,23 +67,123 @@ const deepLinkHandler = new DeepLinkHandler(router);
* @throws {Error} If URL format is invalid
*/
const handleDeepLink = async (data: { url: string }) => {
const { url } = data;
logger.info(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
try {
// Wait for router to be ready
logger.info(`[Main] ⏳ Waiting for router to be ready...`);
await router.isReady();
await deepLinkHandler.handleDeepLink(data.url);
logger.info(`[Main] ✅ Router is ready, processing deeplink`);
// Process the deeplink
logger.info(`[Main] 🚀 Starting deeplink processing`);
await deepLinkHandler.handleDeepLink(url);
logger.info(`[Main] ✅ Deeplink processed successfully`);
} catch (error) {
logger.error("[DeepLink] Error handling deep link: ", error);
logger.error(`[Main] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
// Log additional context for debugging
logger.error(`[Main] 🔍 Debug context:`, {
routerReady: router.isReady(),
currentRoute: router.currentRoute.value,
appMounted: app._instance?.isMounted,
timestamp: new Date().toISOString(),
});
// Fallback to original error handling
let message: string =
error instanceof Error ? error.message : safeStringify(error);
if (data.url) {
message += `\nURL: ${data.url}`;
if (url) {
message += `\nURL: ${url}`;
}
handleApiError({ message } as AxiosError, "deep-link");
}
};
// Register deep link handler with Capacitor
App.addListener("appUrlOpen", handleDeepLink);
// Function to register the deeplink listener
const registerDeepLinkListener = async () => {
try {
logger.info(
`[Main] 🔗 Attempting to register deeplink handler with Capacitor`,
);
logger.log("[Capacitor] Mounting app");
// Check if Capacitor App plugin is available
logger.info(`[Main] 🔍 Checking Capacitor App plugin availability...`);
if (!CapacitorApp) {
throw new Error("Capacitor App plugin not available");
}
logger.info(`[Main] ✅ Capacitor App plugin is available`);
// Check available methods on CapacitorApp
logger.info(
`[Main] 🔍 Capacitor App plugin methods:`,
Object.getOwnPropertyNames(CapacitorApp),
);
logger.info(
`[Main] 🔍 Capacitor App plugin addListener method:`,
typeof CapacitorApp.addListener,
);
// Wait for router to be ready first
await router.isReady();
logger.info(
`[Main] ✅ Router is ready, proceeding with listener registration`,
);
// Try to register the listener
logger.info(`[Main] 🧪 Attempting to register appUrlOpen listener...`);
const listenerHandle = await CapacitorApp.addListener(
"appUrlOpen",
handleDeepLink,
);
logger.info(
`[Main] ✅ appUrlOpen listener registered successfully with handle:`,
listenerHandle,
);
// Test the listener registration by checking if it's actually registered
logger.info(`[Main] 🧪 Verifying listener registration...`);
return listenerHandle;
} catch (error) {
logger.error(`[Main] ❌ Failed to register deeplink listener:`, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
throw error;
}
};
logger.log("[Capacitor] 🚀 Mounting app");
app.mount("#app");
logger.log("[Capacitor] App mounted");
logger.info(`[Main] ✅ App mounted successfully`);
// Register deeplink listener after app is mounted
setTimeout(async () => {
try {
logger.info(
`[Main] ⏳ Delaying listener registration to ensure Capacitor is ready...`,
);
await registerDeepLinkListener();
logger.info(`[Main] 🎉 Deep link system fully initialized!`);
} catch (error) {
logger.error(`[Main] ❌ Deep link system initialization failed:`, error);
}
}, 2000); // 2 second delay to ensure Capacitor is fully ready
// Log app initialization status
setTimeout(() => {
logger.info(`[Main] 📊 App initialization status:`, {
routerReady: router.isReady(),
currentRoute: router.currentRoute.value,
appMounted: app._instance?.isMounted,
timestamp: new Date().toISOString(),
});
}, 1000);

26
src/main.ts Normal file
View File

@@ -0,0 +1,26 @@
/**
* @file Dynamic Main Entry Point
* @author Matthew Raymer
*
* This file dynamically loads the appropriate platform-specific main entry point
* based on the current environment and build configuration.
*/
import { logger } from "./utils/logger";
// Check the platform from environment variables
const platform = process.env.VITE_PLATFORM || "web";
logger.info(`[Main] 🚀 Loading TimeSafari for platform: ${platform}`);
// Dynamically import the appropriate main entry point
if (platform === "capacitor") {
logger.info(`[Main] 📱 Loading Capacitor-specific entry point`);
import("./main.capacitor");
} else if (platform === "electron") {
logger.info(`[Main] 💻 Loading Electron-specific entry point`);
import("./main.electron");
} else {
logger.info(`[Main] 🌐 Loading Web-specific entry point`);
import("./main.web");
}

View File

@@ -321,24 +321,21 @@ const errorHandler = (
router.onError(errorHandler); // Assign the error handler to the router instance
/**
* Global navigation guard to ensure user identity exists
*
* This guard checks if the user has any identities before navigating to most routes.
* If no identity exists, it automatically creates one using the default seed-based method.
*
* Routes that are excluded from this check:
* - /start - Manual identity creation selection
* - /new-identifier - Manual seed-based creation
* - /import-account - Manual import flow
* - /import-derive - Manual derivation flow
* - /database-migration - Migration utilities
* - /deep-link-error - Error page
*
* Navigation guard to ensure user has an identity before accessing protected routes
* @param to - Target route
* @param from - Source route
* @param _from - Source route (unused)
* @param next - Navigation function
*/
router.beforeEach(async (to, _from, next) => {
logger.info(`[Router] 🧭 Navigation guard triggered:`, {
from: _from?.path || "none",
to: to.path,
name: to.name,
params: to.params,
query: to.query,
timestamp: new Date().toISOString(),
});
try {
// Skip identity check for routes that handle identity creation manually
const skipIdentityRoutes = [
@@ -351,32 +348,67 @@ router.beforeEach(async (to, _from, next) => {
];
if (skipIdentityRoutes.includes(to.path)) {
logger.debug(`[Router] ⏭️ Skipping identity check for route: ${to.path}`);
return next();
}
logger.info(`[Router] 🔍 Checking user identity for route: ${to.path}`);
// Check if user has any identities
const allMyDids = await retrieveAccountDids();
logger.info(`[Router] 📋 Found ${allMyDids.length} user identities`);
if (allMyDids.length === 0) {
logger.info("[Router] No identities found, creating default identity");
logger.info("[Router] ⚠️ No identities found, creating default identity");
// Create identity automatically using seed-based method
await generateSaveAndActivateIdentity();
logger.info("[Router] Default identity created successfully");
logger.info("[Router] Default identity created successfully");
} else {
logger.info(
`[Router] ✅ User has ${allMyDids.length} identities, proceeding`,
);
}
logger.info(`[Router] ✅ Navigation guard passed for: ${to.path}`);
next();
} catch (error) {
logger.error(
"[Router] Identity creation failed in navigation guard:",
error,
);
logger.error("[Router] ❌ Identity creation failed in navigation guard:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
route: to.path,
timestamp: new Date().toISOString(),
});
// Redirect to start page if identity creation fails
// This allows users to manually create an identity or troubleshoot
logger.info(
`[Router] 🔄 Redirecting to /start due to identity creation failure`,
);
next("/start");
}
});
// Add navigation success logging
router.afterEach((to, from) => {
logger.info(`[Router] ✅ Navigation completed:`, {
from: from?.path || "none",
to: to.path,
name: to.name,
params: to.params,
query: to.query,
timestamp: new Date().toISOString(),
});
});
// Add error logging
router.onError((error) => {
logger.error(`[Router] ❌ Navigation error:`, {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
timestamp: new Date().toISOString(),
});
});
export default router;

View File

@@ -124,31 +124,32 @@ export class ProfileService {
async deleteProfile(activeDid: string): Promise<boolean> {
try {
const headers = await getHeaders(activeDid);
const response = await this.axios.delete(
`${this.partnerApiServer}/api/partner/userProfile`,
{ headers },
);
const url = `${this.partnerApiServer}/api/partner/userProfile`;
const response = await this.axios.delete(url, { headers });
if (response.status === 200 || response.status === 204) {
if (response.status === 204 || response.status === 200) {
logger.info("Profile deleted successfully");
return true;
} else {
logger.error("Unexpected response status when deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data
data: response.data,
});
throw new Error(`Profile not deleted - HTTP ${response.status}: ${response.statusText}`);
throw new Error(
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
);
}
} catch (error) {
if (this.isApiError(error) && error.response) {
const response = error.response as any; // Type assertion for error response
const response = error.response;
logger.error("API error deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
url: (error as any).config?.url
url: this.getErrorUrl(error),
});
// Handle specific HTTP status codes
if (response.status === 204) {
return true; // 204 is success for DELETE operations
@@ -157,7 +158,11 @@ export class ProfileService {
return true; // Consider this a success if profile doesn't exist
} else if (response.status === 400) {
logger.error("Bad request when deleting profile:", response.data);
throw new Error(`Profile deletion failed: ${response.data?.message || 'Bad request'}`);
const errorMessage =
typeof response.data === "string"
? response.data
: response.data?.message || "Bad request";
throw new Error(`Profile deletion failed: ${errorMessage}`);
} else if (response.status === 401) {
logger.error("Unauthorized to delete profile");
throw new Error("You are not authorized to delete this profile");
@@ -166,7 +171,7 @@ export class ProfileService {
throw new Error("You are not allowed to delete this profile");
}
}
logger.error("Error deleting profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile");
return false;
@@ -236,13 +241,56 @@ export class ProfileService {
}
/**
* Type guard for API errors
* Type guard for API errors with proper typing
*/
private isApiError(
error: unknown,
): error is { response?: { status?: number } } {
private isApiError(error: unknown): error is {
response?: {
status?: number;
statusText?: string;
data?: { message?: string } | string;
};
} {
return typeof error === "object" && error !== null && "response" in error;
}
/**
* Extract error URL safely from error object
*/
private getErrorUrl(error: unknown): string | undefined {
if (this.isAxiosError(error)) {
return error.config?.url;
}
if (this.isApiError(error) && this.hasConfigProperty(error)) {
const config = this.getConfigProperty(error);
return config?.url;
}
return undefined;
}
/**
* Type guard to check if error has config property
*/
private hasConfigProperty(
error: unknown,
): error is { config?: { url?: string } } {
return typeof error === "object" && error !== null && "config" in error;
}
/**
* Safely extract config property from error
*/
private getConfigProperty(error: {
config?: { url?: string };
}): { url?: string } | undefined {
return error.config;
}
/**
* Type guard for AxiosError
*/
private isAxiosError(error: unknown): error is AxiosError {
return error instanceof AxiosError;
}
}
/**

View File

@@ -1,56 +1,22 @@
/**
* @file Deep Link Handler Service
* DeepLinks Service
*
* Handles deep link processing and routing for the TimeSafari application.
* Supports both path parameters and query parameters with comprehensive validation.
*
* @author Matthew Raymer
*
* This service handles the processing and routing of deep links in the TimeSafari app.
* It provides a type-safe interface between the raw deep links and the application router.
*
* Architecture:
* 1. DeepLinkHandler class encapsulates all deep link processing logic
* 2. Uses Zod schemas from interfaces/deepLinks for parameter validation
* 3. Provides consistent error handling and logging
* 4. Maps validated parameters to Vue router calls
*
* Error Handling Strategy:
* - All errors are wrapped in DeepLinkError interface
* - Errors include error codes for systematic handling
* - Detailed error information is logged for debugging
* - Errors are propagated to the global error handler
*
* Validation Strategy:
* - URL structure validation
* - Route-specific parameter validation using Zod schemas
* - Query parameter validation and sanitization
* - Type-safe parameter passing to router
*
* Deep Link Format:
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
*
* Supported Routes:
* - claim: View claim
* - claim-add-raw: Add raw claim
* - 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);
* await handler.handleDeepLink("timesafari://claim/123?view=details");
* @version 2.0.0
* @since 2025-01-25
*/
import { Router } from "vue-router";
import { z } from "zod";
import {
deepLinkSchemas,
baseUrlSchema,
deepLinkPathSchemas,
routeSchema,
DeepLinkRoute,
deepLinkQuerySchemas,
} from "../interfaces/deepLinks";
import type { DeepLinkError } from "../interfaces/deepLinks";
import { logger } from "../utils/logger";
@@ -74,7 +40,7 @@ function getFirstKeyFromZodObject(
* 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(
Object.entries(deepLinkPathSchemas).reduce(
(acc, [routeName, schema]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
@@ -103,83 +69,152 @@ export class DeepLinkHandler {
}
/**
* Parses deep link URL into path, params and query components.
* Validates URL structure using Zod schemas.
*
* @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
* Main entry point for processing deep links
* @param url - The deep link URL to process
* @throws {DeepLinkError} If validation fails or route is invalid
*/
private parseDeepLink(url: string) {
const parts = url.split("://");
if (parts.length !== 2) {
throw { code: "INVALID_URL", message: "Invalid URL format" };
}
async handleDeepLink(url: string): Promise<void> {
logger.info(`[DeepLink] 🚀 Starting deeplink processing for URL: ${url}`);
// Validate base URL structure
baseUrlSchema.parse({
scheme: parts[0],
path: parts[1],
queryParams: {}, // Will be populated below
});
try {
logger.info(`[DeepLink] 📍 Parsing URL: ${url}`);
const { path, params, query } = this.parseDeepLink(url);
const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/");
// Validate route exists before proceeding
if (!ROUTE_MAP[routePath]) {
throw {
code: "INVALID_ROUTE",
message: `Invalid route path: ${routePath}`,
details: { routePath },
};
}
const query: Record<string, string> = {};
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => {
query[key] = value;
logger.info(`[DeepLink] ✅ URL parsed successfully:`, {
path,
params: Object.keys(params),
query: Object.keys(query),
fullParams: params,
fullQuery: query,
});
}
const params: Record<string, string> = {};
if (pathParams) {
// Now we know routePath exists in ROUTE_MAP
const routeConfig = ROUTE_MAP[routePath];
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
}
// Sanitize parameters (remove undefined values)
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
// logConsoleAndDb(
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`,
// false,
// );
return { path: routePath, params, query };
logger.info(`[DeepLink] 🧹 Parameters sanitized:`, sanitizedParams);
await this.validateAndRoute(path, sanitizedParams, query);
logger.info(`[DeepLink] 🎯 Deeplink processing completed successfully`);
} catch (error) {
logger.error(`[DeepLink] ❌ Deeplink processing failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
const deepLinkError = error as DeepLinkError;
throw deepLinkError;
}
}
/**
* Routes the deep link to appropriate view with validated parameters.
* Validates route and parameters using Zod schemas before routing.
*
* @param path - The route path from the deep link
* @param params - URL parameters
* @param query - Query string parameters
* @throws {DeepLinkError} If validation fails or route is invalid
* Parse a deep link URL into its components
* @param url - The deep link URL
* @returns Parsed components
*/
private parseDeepLink(url: string): {
path: string;
params: Record<string, string>;
query: Record<string, string>;
} {
logger.debug(`[DeepLink] 🔍 Parsing deep link: ${url}`);
try {
const parts = url.split("://");
if (parts.length !== 2) {
throw new Error("Invalid URL format");
}
const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/");
// Parse path parameters using route-specific configuration
const params: Record<string, string> = {};
if (pathParams.length > 0) {
// Get the correct parameter key for this route
const routeConfig = ROUTE_MAP[routePath];
if (routeConfig?.paramKey) {
params[routeConfig.paramKey] = pathParams[0];
logger.debug(
`[DeepLink] 📍 Path parameter extracted: ${routeConfig.paramKey}=${pathParams[0]}`,
);
} else {
// Fallback to 'id' for backward compatibility
params.id = pathParams[0];
logger.debug(
`[DeepLink] 📍 Path parameter extracted: id=${pathParams[0]} (fallback)`,
);
}
}
// Parse query parameters
const query: Record<string, string> = {};
if (queryString) {
const queryParams = new URLSearchParams(queryString);
for (const [key, value] of queryParams.entries()) {
query[key] = value;
}
logger.debug(`[DeepLink] 🔗 Query parameters extracted:`, query);
}
logger.info(`[DeepLink] ✅ Parse completed:`, {
routePath,
pathParams: pathParams.length,
queryParams: Object.keys(query).length,
});
return { path: routePath, params, query };
} catch (error) {
logger.error(`[DeepLink] ❌ Parse failed:`, {
url,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
/**
* Validate and route the deep link
* @param path - The route path
* @param params - Path parameters
* @param query - Query parameters
*/
private async validateAndRoute(
path: string,
params: Record<string, string>,
query: Record<string, string>,
): Promise<void> {
logger.info(
`[DeepLink] 🎯 Starting validation and routing for path: ${path}`,
);
// First try to validate the route path
let routeName: string;
try {
logger.debug(`[DeepLink] 🔍 Validating route path: ${path}`);
// Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name;
logger.info(`[DeepLink] ✅ Route validation passed: ${validRoute}`);
// Get route configuration
const routeConfig = ROUTE_MAP[validRoute];
logger.info(`[DeepLink] 📋 Route config retrieved:`, routeConfig);
if (!routeConfig) {
logger.error(`[DeepLink] ❌ No route config found for: ${validRoute}`);
throw new Error(`Route configuration missing for: ${validRoute}`);
}
routeName = routeConfig.name;
logger.info(`[DeepLink] 🎯 Route name resolved: ${routeName}`);
} catch (error) {
logger.error(`[DeepLink] Invalid route path: ${path}`);
logger.error(`[DeepLink] ❌ Route validation failed:`, {
path,
error: error instanceof Error ? error.message : String(error),
});
// Redirect to error page with information about the invalid link
await this.router.replace({
@@ -193,21 +228,66 @@ export class DeepLinkHandler {
},
});
// This previously threw an error but we're redirecting so there's no need.
logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid route: ${path}`,
);
return;
}
// Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
// Continue with parameter validation
logger.info(
`[DeepLink] 🔍 Starting parameter validation for route: ${routeName}`,
);
const pathSchema =
deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
const querySchema =
deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
logger.debug(`[DeepLink] 📋 Schemas found:`, {
hasPathSchema: !!pathSchema,
hasQuerySchema: !!querySchema,
pathSchemaType: pathSchema ? typeof pathSchema : "none",
querySchemaType: querySchema ? typeof querySchema : "none",
});
let validatedPathParams: Record<string, string> = {};
let validatedQueryParams: Record<string, string> = {};
let validatedParams;
try {
validatedParams = await schema.parseAsync(params);
if (pathSchema) {
logger.debug(`[DeepLink] 🔍 Validating path parameters:`, params);
validatedPathParams = await pathSchema.parseAsync(params);
logger.info(
`[DeepLink] ✅ Path parameters validated:`,
validatedPathParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No path schema found for: ${path}`);
validatedPathParams = params;
}
if (querySchema) {
logger.debug(`[DeepLink] 🔍 Validating query parameters:`, query);
validatedQueryParams = await querySchema.parseAsync(query);
logger.info(
`[DeepLink] ✅ Query parameters validated:`,
validatedQueryParams,
);
} else {
logger.debug(`[DeepLink] ⚠️ No query schema found for: ${path}`);
validatedQueryParams = query;
}
} catch (error) {
// For parameter validation errors, provide specific error feedback
logger.error(
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
);
logger.error(`[DeepLink] ❌ Parameter validation failed:`, {
routeName,
path,
params,
query,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
await this.router.replace({
name: "deep-link-error",
params,
@@ -219,58 +299,52 @@ export class DeepLinkHandler {
},
});
// This previously threw an error but we're redirecting so there's no need.
logger.info(
`[DeepLink] 🔄 Redirected to error page for invalid parameters`,
);
return;
}
// Attempt navigation
try {
logger.info(`[DeepLink] 🚀 Attempting navigation:`, {
routeName,
pathParams: validatedPathParams,
queryParams: validatedQueryParams,
});
await this.router.replace({
name: routeName,
params: validatedParams,
params: validatedPathParams,
query: validatedQueryParams,
});
logger.info(`[DeepLink] ✅ Navigation successful to: ${routeName}`);
} catch (error) {
logger.error(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`,
);
// For parameter validation errors, provide specific error feedback
logger.error(`[DeepLink] ❌ Navigation failed:`, {
routeName,
path,
validatedPathParams,
validatedQueryParams,
error: error instanceof Error ? error.message : String(error),
errorDetails: JSON.stringify(error),
});
// Redirect to error page for navigation failures
await this.router.replace({
name: "deep-link-error",
params: validatedParams,
params: validatedPathParams,
query: {
originalPath: path,
errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
errorMessage: `Error routing to ${routeName}: ${(error as Error).message}`,
...validatedQueryParams,
},
});
}
}
/**
* 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<void> {
try {
const { path, params, query } = this.parseDeepLink(url);
// Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
logger.info(
`[DeepLink] 🔄 Redirected to error page for navigation failure`,
);
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logger.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
);
throw {
code: deepLinkError.code || "UNKNOWN_ERROR",
message: deepLinkError.message,
details: deepLinkError.details,
};
}
}
}

View File

@@ -174,15 +174,15 @@
:aria-busy="loadingProfile || savingProfile"
></textarea>
<div class="flex items-center mb-4">
<input
v-model="includeUserProfileLocation"
type="checkbox"
class="mr-2"
@change="onLocationCheckboxChange"
/>
<label for="includeUserProfileLocation">Include Location</label>
</div>
<div class="flex items-center mb-4">
<input
v-model="includeUserProfileLocation"
type="checkbox"
class="mr-2"
@change="onLocationCheckboxChange"
/>
<label for="includeUserProfileLocation">Include Location</label>
</div>
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500">
The location you choose will be shared with the world until you remove
@@ -917,15 +917,21 @@ export default class AccountViewView extends Vue {
created() {
this.notify = createNotifyHelpers(this.$notify);
// Fix Leaflet icon issues in modern bundlers
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
if (L.Icon.Default) {
delete (L.Icon.Default.prototype as any)._getIconUrl;
// Type-safe way to handle Leaflet icon prototype
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
if ("_getIconUrl" in iconDefault) {
delete iconDefault._getIconUrl;
}
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
iconRetinaUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png",
iconUrl: "https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png",
shadowUrl:
"https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png",
});
}
}
@@ -954,7 +960,7 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = profile.latitude;
this.userProfileLongitude = profile.longitude;
this.includeUserProfileLocation = profile.includeLocation;
// Initialize map ready state if location is included
if (profile.includeLocation) {
this.isMapReady = false; // Will be set to true when map is ready
@@ -1541,11 +1547,18 @@ export default class AccountViewView extends Vue {
onMapReady(map: L.Map): void {
try {
// doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
const zoom =
this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
const lat = this.userProfileLatitude || 0;
const lng = this.userProfileLongitude || 0;
map.setView([lat, lng], zoom);
this.isMapReady = true;
logger.debug(
"Map ready state set to true, coordinates:",
[lat, lng],
"zoom:",
zoom,
);
} catch (error) {
logger.error("Error in onMapReady:", error);
this.isMapReady = true; // Set to true even on error to prevent infinite loading
@@ -1553,6 +1566,11 @@ export default class AccountViewView extends Vue {
}
onMapMounted(): void {
logger.debug("Map component mounted");
// Check if map ref is available
const mapRef = this.$refs.profileMap;
logger.debug("Map ref:", mapRef);
// Try to set map ready after component is mounted
setTimeout(() => {
this.isMapReady = true;
@@ -1585,7 +1603,9 @@ export default class AccountViewView extends Vue {
longitude: this.userProfileLongitude,
includeLocation: this.includeUserProfileLocation,
};
logger.debug("Saving profile data:", profileData);
const success = await this.profileService.saveProfile(
this.activeDid,
profileData,
@@ -1614,7 +1634,7 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = updated.latitude;
this.userProfileLongitude = updated.longitude;
this.includeUserProfileLocation = updated.includeLocation;
// Reset map ready state when toggling location
if (!updated.includeLocation) {
this.isMapReady = false;
@@ -1663,7 +1683,7 @@ export default class AccountViewView extends Vue {
}
} catch (error) {
logger.error("Error in deleteProfile component method:", error);
// Show more specific error message if available
if (error instanceof Error) {
this.notify.error(error.message);
@@ -1694,6 +1714,10 @@ export default class AccountViewView extends Vue {
onLocationCheckboxChange(): void {
try {
logger.debug(
"Location checkbox changed, new value:",
this.includeUserProfileLocation,
);
if (!this.includeUserProfileLocation) {
// Location checkbox was unchecked, clean up map state
this.isMapReady = false;
@@ -1702,14 +1726,15 @@ export default class AccountViewView extends Vue {
} else {
// Location checkbox was checked, start map initialization timeout
this.isMapReady = false;
logger.debug("Location checked, starting map initialization timeout");
// Try to set map ready after a short delay to allow Vue to render
setTimeout(() => {
if (!this.isMapReady) {
this.isMapReady = true;
}
}, 1000); // 1 second delay
this.handleMapInitFailure();
}
} catch (error) {

View File

@@ -47,7 +47,7 @@ import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
VALID_DEEP_LINK_ROUTES,
deepLinkSchemas,
deepLinkPathSchemas,
} from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger";
@@ -56,7 +56,7 @@ const route = useRoute();
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]) => {
Object.entries(deepLinkPathSchemas).map(([route, schema]) => {
const param = Object.keys(schema.shape)[0];
return [route, param];
}),

View File

@@ -476,7 +476,7 @@ export default class HomeView extends Vue {
// Re-initialize identity with new settings (loads settings internally)
await this.initializeIdentity();
} else {
logger.info(
logger.debug(
"[HomeView Settings Trace] 📍 DID unchanged, skipping re-initialization",
);
}
@@ -756,17 +756,34 @@ export default class HomeView extends Vue {
* Called by FeedFilters component when filters change
*/
async reloadFeedOnChange() {
const settings = await this.$accountSettings(this.activeDid, {
filterFeedByVisible: false,
filterFeedByNearby: false,
logger.debug("[HomeView] 🔄 reloadFeedOnChange() called - refreshing feed");
// Get current settings without overriding with defaults
const settings = await this.$accountSettings(this.activeDid);
logger.debug("[HomeView] 📊 Current filter settings:", {
filterFeedByVisible: settings.filterFeedByVisible,
filterFeedByNearby: settings.filterFeedByNearby,
searchBoxes: settings.searchBoxes?.length || 0,
});
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
logger.debug("[HomeView] 🎯 Updated filter states:", {
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
});
this.feedData = [];
this.feedPreviousOldestId = undefined;
logger.debug("[HomeView] 🧹 Cleared feed data, calling updateAllFeed()");
await this.updateAllFeed();
logger.debug("[HomeView] ✅ Feed refresh completed");
}
/**
@@ -845,6 +862,14 @@ export default class HomeView extends Vue {
* - this.feedLastViewedClaimId (via updateFeedLastViewedId)
*/
async updateAllFeed() {
logger.debug("[HomeView] 🚀 updateAllFeed() called", {
isFeedLoading: this.isFeedLoading,
currentFeedDataLength: this.feedData.length,
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
});
this.isFeedLoading = true;
let endOfResults = true;
@@ -853,21 +878,37 @@ export default class HomeView extends Vue {
this.apiServer,
this.feedPreviousOldestId,
);
logger.debug("[HomeView] 📡 Retrieved gives from API", {
resultsCount: results.data.length,
endOfResults,
});
if (results.data.length > 0) {
endOfResults = false;
// gather any contacts that user has blocked from view
await this.processFeedResults(results.data);
await this.updateFeedLastViewedId(results.data);
logger.debug("[HomeView] 📝 Processed feed results", {
processedCount: this.feedData.length,
});
}
} catch (e) {
logger.error("[HomeView] ❌ Error in updateAllFeed:", e);
this.handleFeedError(e);
}
if (this.feedData.length === 0 && !endOfResults) {
logger.debug("[HomeView] 🔄 No results after filtering, retrying...");
await this.updateAllFeed();
}
this.isFeedLoading = false;
logger.debug("[HomeView] ✅ updateAllFeed() completed", {
finalFeedDataLength: this.feedData.length,
isFeedLoading: this.isFeedLoading,
});
}
/**
@@ -892,12 +933,35 @@ export default class HomeView extends Vue {
* @param records Array of feed records to process
*/
private async processFeedResults(records: GiveSummaryRecord[]) {
logger.debug("[HomeView] 📝 Processing feed results:", {
inputRecords: records.length,
currentFilters: {
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
},
});
let processedCount = 0;
let filteredCount = 0;
for (const record of records) {
const processedRecord = await this.processRecord(record);
if (processedRecord) {
this.feedData.push(processedRecord);
processedCount++;
} else {
filteredCount++;
}
}
logger.debug("[HomeView] 📊 Feed processing results:", {
processed: processedCount,
filtered: filteredCount,
total: records.length,
finalFeedLength: this.feedData.length,
});
this.feedPreviousOldestId = records[records.length - 1].jwtId;
}
@@ -931,7 +995,7 @@ export default class HomeView extends Vue {
* - this.feedData (via createFeedRecord)
*
* @param record The record to process
* @returns Processed record with contact info if it passes filters, null otherwise
* @returns Processed record if it passes filters, null otherwise
*/
private async processRecord(
record: GiveSummaryRecord,
@@ -941,13 +1005,28 @@ export default class HomeView extends Vue {
const recipientDid = this.extractRecipientDid(claim);
const fulfillsPlan = await this.getFulfillsPlan(record);
// Log record details for debugging
logger.debug("[HomeView] 🔍 Processing record:", {
recordId: record.jwtId,
hasFulfillsPlan: !!fulfillsPlan,
fulfillsPlanHandleId: record.fulfillsPlanHandleId,
filters: {
isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByNearby,
isFeedFilteredByNearby: this.isFeedFilteredByNearby,
},
});
if (!this.shouldIncludeRecord(record, fulfillsPlan)) {
logger.debug("[HomeView] ❌ Record filtered out:", record.jwtId);
return null;
}
const provider = this.extractProvider(claim);
const providedByPlan = await this.getProvidedByPlan(provider);
logger.debug("[HomeView] ✅ Record included:", record.jwtId);
return this.createFeedRecord(
record,
claim,
@@ -1096,6 +1175,22 @@ export default class HomeView extends Vue {
}
}
// Add debug logging for nearby filter
if (this.isFeedFilteredByNearby && record.fulfillsPlanHandleId) {
logger.debug("[HomeView] 🔍 Nearby filter check:", {
recordId: record.jwtId,
hasFulfillsPlan: !!fulfillsPlan,
hasLocation: !!(fulfillsPlan?.locLat && fulfillsPlan?.locLon),
location: fulfillsPlan
? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon }
: null,
inSearchBox: fulfillsPlan
? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon)
: null,
finalResult: anyMatch,
});
}
return anyMatch;
}
@@ -1531,7 +1626,10 @@ export default class HomeView extends Vue {
* Called by template click handler
*/
openFeedFilters() {
(this.$refs.feedFilters as FeedFilters).open(this.reloadFeedOnChange);
(this.$refs.feedFilters as FeedFilters).open(
this.reloadFeedOnChange,
this.activeDid,
);
}
/**

View File

@@ -113,7 +113,7 @@ export default class OnboardMeetingMembersView extends Vue {
try {
// Identity creation should be handled by router guard, but keep as fallback for meeting setup
if (!this.activeDid) {
logger.info(
this.$logAndConsole(
"[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup",
);
this.activeDid = await generateSaveAndActivateIdentity();