You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

662 lines
19 KiB

import {
createRouter,
createWebHistory,
createMemoryHistory,
NavigationGuardNext,
RouteLocationNormalized,
RouteRecordRaw,
} from "vue-router";
import {
accountsDBPromise,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { logger } from "../utils/logger";
import { Component as VueComponent } from "vue-facing-decorator";
import { defineComponent } from "vue";
/**
*
* @param to :RouteLocationNormalized
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
};
const routes: Array<RouteRecordRaw> = [
{
path: "/account",
name: "account",
component: () => {
logger.log("Starting lazy load of AccountViewView");
return new Promise((resolve) => {
import("../views/AccountViewView.vue")
.then((module) => {
if (!module?.default) {
logger.error(
"AccountViewView module loaded but default export is missing",
{
module: {
hasDefault: !!module?.default,
keys: Object.keys(module || {}),
},
},
);
resolve(createErrorComponent());
return;
}
// Check if the component has the required dependencies
const component = module.default;
logger.log("AccountViewView loaded, checking dependencies...", {
componentName: component.name,
hasVueComponent: component instanceof VueComponent,
hasClass: typeof component === "function",
type: typeof component,
});
resolve(component);
})
.catch((err) => {
logger.error("Failed to load AccountViewView:", {
error:
err instanceof Error
? {
name: err.name,
message: err.message,
stack: err.stack,
}
: err,
type: typeof err,
});
resolve(createErrorComponent());
});
});
},
beforeEnter: async (to, from, next) => {
try {
logger.log("Account route beforeEnter guard starting");
// Check if required dependencies are available
const settings = await retrieveSettingsForActiveAccount();
logger.log("Account route: settings loaded", {
hasActiveDid: !!settings.activeDid,
isRegistered: !!settings.isRegistered,
});
next();
} catch (error) {
logger.error("Error in account route beforeEnter:", {
error:
error instanceof Error
? {
name: error.name,
message: error.message,
stack: error.stack,
}
: error,
});
next({ name: "home" });
}
},
},
{
path: "/claim/:id?",
name: "claim",
component: () => import("../views/ClaimView.vue"),
},
{
path: "/claim-add-raw/:id?",
name: "claim-add-raw",
component: () => import("../views/ClaimAddRawView.vue"),
},
{
path: "/claim-cert/:id",
name: "claim-cert",
component: () => import("../views/ClaimCertificateView.vue"),
},
{
path: "/confirm-contact",
name: "confirm-contact",
component: () => import("../views/ConfirmContactView.vue"),
},
{
path: "/confirm-gift/:id?",
name: "confirm-gift",
component: () => import("../views/ConfirmGiftView.vue"),
},
{
path: "/contact-amounts",
name: "contact-amounts",
component: () => import("../views/ContactAmountsView.vue"),
},
{
path: "/contact-edit/:did",
name: "contact-edit",
component: () => import("../views/ContactEditView.vue"),
},
{
path: "/contact-gift",
name: "contact-gift",
component: () => import("../views/ContactGiftingView.vue"),
},
{
path: "/contact-import/:jwt?",
name: "contact-import",
component: () => import("../views/ContactImportView.vue"),
},
{
path: "/contact-qr",
name: "contact-qr",
component: () => import("../views/ContactQRScanShowView.vue"),
},
{
path: "/contacts",
name: "contacts",
component: () => import("../views/ContactsView.vue"),
},
{
path: "/did/:did?",
name: "did",
component: () => import("../views/DIDView.vue"),
},
{
path: "/discover",
name: "discover",
component: () => import("../views/DiscoverView.vue"),
},
{
path: "/gifted-details",
name: "gifted-details",
component: () => import("../views/GiftedDetailsView.vue"),
},
{
path: "/help",
name: "help",
component: () => import("../views/HelpView.vue"),
},
{
path: "/help-notifications",
name: "help-notifications",
component: () => import("../views/HelpNotificationsView.vue"),
},
{
path: "/help-notification-types",
name: "help-notification-types",
component: () => import("../views/HelpNotificationTypesView.vue"),
},
{
path: "/help-onboarding",
name: "help-onboarding",
component: () => import("../views/HelpOnboardingView.vue"),
},
{
path: "/",
name: "home",
component: () => import("../views/HomeView.vue"),
},
{
path: "/identity-switcher",
name: "identity-switcher",
component: () => import("../views/IdentitySwitcherView.vue"),
},
{
path: "/import-account",
name: "import-account",
component: () => import("../views/ImportAccountView.vue"),
},
{
path: "/import-derive",
name: "import-derive",
component: () => import("../views/ImportDerivedAccountView.vue"),
},
{
path: "/invite-one",
name: "invite-one",
component: () => import("../views/InviteOneView.vue"),
},
{
path: "/invite-one-accept/:jwt?",
name: "InviteOneAcceptView",
component: () => import("../views/InviteOneAcceptView.vue"),
},
{
path: "/logs",
name: "logs",
component: () => import("../views/LogView.vue"),
},
{
path: "/new-activity",
name: "new-activity",
component: () => import("../views/NewActivityView.vue"),
},
{
path: "/new-edit-account",
name: "new-edit-account",
component: () => import("../views/NewEditAccountView.vue"),
},
{
path: "/new-edit-project",
name: "new-edit-project",
component: () => import("../views/NewEditProjectView.vue"),
},
{
path: "/new-identifier",
name: "new-identifier",
component: () => import("../views/NewIdentifierView.vue"),
},
{
path: "/offer-details/:id?",
name: "offer-details",
component: () => import("../views/OfferDetailsView.vue"),
},
{
path: "/onboard-meeting-list",
name: "onboard-meeting-list",
component: () => import("../views/OnboardMeetingListView.vue"),
},
{
path: "/onboard-meeting-members/:groupId",
name: "onboard-meeting-members",
component: () => import("../views/OnboardMeetingMembersView.vue"),
},
{
path: "/onboard-meeting-setup",
name: "onboard-meeting-setup",
component: () => import("../views/OnboardMeetingSetupView.vue"),
},
{
path: "/project/:id?",
name: "project",
component: () => import("../views/ProjectViewView.vue"),
},
{
path: "/projects",
name: "projects",
component: () => import("../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/quick-action-bvc",
name: "quick-action-bvc",
component: () => import("../views/QuickActionBvcView.vue"),
},
{
path: "/quick-action-bvc-begin",
name: "quick-action-bvc-begin",
component: () => import("../views/QuickActionBvcBeginView.vue"),
},
{
path: "/quick-action-bvc-end",
name: "quick-action-bvc-end",
component: () => import("../views/QuickActionBvcEndView.vue"),
},
{
path: "/recent-offers-to-user",
name: "recent-offers-to-user",
component: () => import("../views/RecentOffersToUserView.vue"),
},
{
path: "/recent-offers-to-user-projects",
name: "recent-offers-to-user-projects",
component: () => import("../views/RecentOffersToUserProjectsView.vue"),
},
{
path: "/scan-contact",
name: "scan-contact",
component: () => import("../views/ContactScanView.vue"),
},
{
path: "/search-area",
name: "search-area",
component: () => import("../views/SearchAreaView.vue"),
},
{
path: "/seed-backup",
name: "seed-backup",
component: () => import("../views/SeedBackupView.vue"),
},
{
path: "/share-my-contact-info",
name: "share-my-contact-info",
component: () => import("../views/ShareMyContactInfoView.vue"),
},
{
path: "/shared-photo",
name: "shared-photo",
component: () => import("../views/SharedPhotoView.vue"),
},
// /share-target is also an endpoint in the service worker
{
path: "/start",
name: "start",
component: () => import("../views/StartView.vue"),
},
{
path: "/statistics",
name: "statistics",
component: () => import("../views/StatisticsView.vue"),
},
{
path: "/test",
name: "test",
component: () => import("../views/TestView.vue"),
},
{
path: "/user-profile/:id?",
name: "user-profile",
component: () => import("../views/UserProfileView.vue"),
},
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
];
const isElectron = window.location.protocol === "file:";
const initialPath = isElectron
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
: window.location.pathname;
const history = isElectron
? createMemoryHistory() // Memory history for Electron
: createWebHistory("/"); // Add base path for web apps
/** @type {*} */
const router = createRouter({
history,
routes,
});
// Replace initial URL to start at `/` if necessary
router.replace(initialPath || "/");
// Add global error handler
router.onError((error, to, from) => {
logger.error("Router error:", {
error:
error instanceof Error
? {
name: error.name,
message: error.message,
stack: error.stack,
}
: error,
to: {
name: to.name,
path: to.path,
},
from: {
name: from.name,
path: from.path,
},
});
// If it's a reference error during account view import, try to handle it gracefully
if (error instanceof ReferenceError && to.name === "account") {
logger.error("Account view import error:", {
error:
error instanceof Error
? {
name: error.name,
message: error.message,
stack: error.stack,
}
: error,
});
// Instead of redirecting, let the component's error handling take over
return;
}
});
// Add navigation guard for debugging
router.beforeEach((to, from, next) => {
logger.log("Navigation debug:", {
to: {
fullPath: to.fullPath,
path: to.path,
name: to.name,
params: to.params,
query: to.query,
},
from: {
fullPath: from.fullPath,
path: from.path,
name: from.name,
params: from.params,
query: from.query,
},
});
// For account route, try to preload the component
if (to.name === "account") {
logger.log("Preloading account component...");
// Wrap in try-catch and use Promise
new Promise((resolve) => {
logger.log("Starting dynamic import of AccountViewView");
// Add immediate try-catch to get more context
try {
const importPromise = import("../views/AccountViewView.vue");
logger.log("Import initiated successfully");
importPromise
.then((module) => {
try {
logger.log("Import completed, analyzing module:", {
moduleExists: !!module,
moduleType: typeof module,
moduleKeys: Object.keys(module || {}),
hasDefault: !!module?.default,
defaultType: module?.default
? typeof module.default
: "undefined",
defaultConstructor: module?.default?.constructor?.name,
moduleContent: {
...Object.fromEntries(
Object.entries(module).map(([key, value]) => [
key,
typeof value === "function"
? "function"
: typeof value === "object"
? Object.keys(value || {})
: typeof value,
]),
),
},
});
if (!module?.default) {
logger.error(
"AccountViewView preload: module loaded but default export is missing",
{
module: {
hasDefault: !!module?.default,
keys: Object.keys(module || {}),
moduleType: typeof module,
exports: Object.keys(module || {}).map((key) => ({
key,
type: typeof (module as any)[key],
value:
typeof (module as any)[key] === "function"
? "function"
: typeof (module as any)[key] === "object"
? Object.keys((module as any)[key] || {})
: (module as any)[key],
})),
},
},
);
resolve(null);
return;
}
const component = module.default;
// Try to safely inspect the component
const componentDetails = {
componentName: component.name,
hasVueComponent: component instanceof VueComponent,
hasClass: typeof component === "function",
type: typeof component,
properties: Object.keys(component),
decorators: Object.getOwnPropertyDescriptor(
component,
"__decorators",
),
vueOptions:
(component as any).__vccOpts ||
(component as any).options ||
null,
setup: typeof (component as any).setup === "function",
render: typeof (component as any).render === "function",
components: (component as any).components
? Object.keys((component as any).components)
: null,
imports: Object.keys(module).filter((key) => key !== "default"),
};
logger.log("Successfully analyzed component:", componentDetails);
resolve(component);
} catch (analysisError) {
logger.error("Error during component analysis:", {
error:
analysisError instanceof Error
? {
name: analysisError.name,
message: analysisError.message,
stack: analysisError.stack,
keys: Object.keys(analysisError),
properties: Object.getOwnPropertyNames(analysisError),
}
: analysisError,
type: typeof analysisError,
phase: "analysis",
});
resolve(null);
}
})
.catch((err) => {
logger.error("Failed to preload account component:", {
error:
err instanceof Error
? {
name: err.name,
message: err.message,
stack: err.stack,
keys: Object.keys(err),
properties: Object.getOwnPropertyNames(err),
}
: err,
type: typeof err,
context: {
routeName: to.name,
routePath: to.path,
fromRoute: from.name,
},
phase: "module-load",
});
resolve(null);
});
} catch (immediateError) {
logger.error("Immediate error during import initiation:", {
error:
immediateError instanceof Error
? {
name: immediateError.name,
message: immediateError.message,
stack: immediateError.stack,
keys: Object.keys(immediateError),
properties: Object.getOwnPropertyNames(immediateError),
}
: immediateError,
type: typeof immediateError,
context: {
routeName: to.name,
routePath: to.path,
fromRoute: from.name,
importPath: "../views/AccountViewView.vue",
},
phase: "import",
});
resolve(null);
}
}).catch((err) => {
logger.error("Critical error in account component preload:", {
error:
err instanceof Error
? {
name: err.name,
message: err.message,
stack: err.stack,
}
: err,
context: {
routeName: to.name,
routePath: to.path,
fromRoute: from.name,
},
phase: "wrapper",
});
});
}
// Always call next() to continue navigation
next();
});
function createErrorComponent() {
return defineComponent({
name: "AccountViewError",
components: {
// Add any required components here
},
setup() {
const goHome = () => {
router.push({ name: "home" });
};
return {
goHome,
};
},
template: `
<section class="p-6 pb-24 max-w-3xl mx-auto">
<h1 class="text-4xl text-center font-light mb-8">Error Loading Account View</h1>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">Failed to load account view.</strong>
<span class="block sm:inline"> Please try refreshing the page.</span>
</div>
<div class="mt-4 text-center">
<button @click="goHome" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Return to Home
</button>
</div>
</section>
`,
});
}
export default router;