forked from jsnbuchanan/crowd-funder-for-time-pwa
- Add comprehensive route validation with zod schema - Create type-safe DeepLinkRoute enum for all valid routes - Add structured error handling for invalid routes - Redirect to error page with detailed feedback - Add better timeout handling in deeplink tests The changes improve robustness by: 1. Validating route paths before navigation 2. Providing detailed error messages for invalid links 3. Redirecting users to dedicated error pages 4. Adding parameter validation with specific feedback 5. Improving type safety across deeplink handling
335 lines
8.5 KiB
TypeScript
335 lines
8.5 KiB
TypeScript
import {
|
|
createRouter,
|
|
createWebHistory,
|
|
createMemoryHistory,
|
|
NavigationGuardNext,
|
|
RouteLocationNormalized,
|
|
RouteRecordRaw,
|
|
} from "vue-router";
|
|
import { accountsDBPromise } from "../db/index";
|
|
import { logger } from "../utils/logger";
|
|
|
|
/**
|
|
*
|
|
* @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: () => import("../views/AccountViewView.vue"),
|
|
},
|
|
{
|
|
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: "/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 || "/");
|
|
|
|
const errorHandler = (
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
error: any,
|
|
to: RouteLocationNormalized,
|
|
from: RouteLocationNormalized,
|
|
) => {
|
|
// Handle the error here
|
|
logger.error("Caught in top level error handler:", error, to, from);
|
|
alert("Something is very wrong. Try reloading or restarting the app.");
|
|
|
|
// You can also perform additional actions, such as displaying an error message or redirecting the user to a specific page
|
|
};
|
|
|
|
router.onError(errorHandler); // Assign the error handler to the router instance
|
|
|
|
// router.beforeEach((to, from, next) => {
|
|
// console.log("Navigating to view:", to.name);
|
|
// console.log("From view:", from.name);
|
|
// next();
|
|
// });
|
|
|
|
export default router;
|