Merge branch 'master' into ios-contact-copy
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,154 @@
|
||||
/** * @file RegistrationNotice.vue * @description Reusable component for
|
||||
displaying user registration status and related actions. * Shows registration
|
||||
notice when user is not registered, with options to show identifier info * or
|
||||
access advanced options. * * @author Jose Olarte III * @version 1.0.0 * @created
|
||||
2025-08-21T17:25:28-08:00 */
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="!isRegistered && show"
|
||||
id="noticeBeforeAnnounce"
|
||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 mt-4"
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
id="noticeSomeoneMustRegisterYou"
|
||||
class="bg-amber-200 text-amber-900 border-amber-500 border-dashed border text-center rounded-md overflow-hidden px-4 py-3 my-4"
|
||||
>
|
||||
<p class="mb-4">
|
||||
Before you can publicly announce a new project or time commitment, a
|
||||
friend needs to register you.
|
||||
</p>
|
||||
<button
|
||||
class="inline-block text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="shareInfo"
|
||||
>
|
||||
Share Your Info
|
||||
</button>
|
||||
<p class="mb-4">{{ message }}</p>
|
||||
<div class="grid grid-cols-1 gap-2 sm:flex sm:justify-center">
|
||||
<button
|
||||
class="inline-block text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="showNameThenIdDialog"
|
||||
>
|
||||
Show them {{ passkeysEnabled ? "default" : "your" }} identifier info
|
||||
</button>
|
||||
<button
|
||||
class="inline-block text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||
@click="openAdvancedOptions"
|
||||
>
|
||||
See advanced options
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<UserNameDialog ref="userNameDialog" />
|
||||
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, Emit } from "vue-facing-decorator";
|
||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import UserNameDialog from "./UserNameDialog.vue";
|
||||
import ChoiceButtonDialog from "./ChoiceButtonDialog.vue";
|
||||
|
||||
@Component({ name: "RegistrationNotice" })
|
||||
/**
|
||||
* RegistrationNotice Component
|
||||
*
|
||||
* Displays registration status notice and provides actions for unregistered users.
|
||||
* Handles all registration-related flows internally without requiring parent component intervention.
|
||||
*
|
||||
* Template Usage:
|
||||
* ```vue
|
||||
* <RegistrationNotice
|
||||
* v-if="!isUserRegistered"
|
||||
* :passkeys-enabled="PASSKEYS_ENABLED"
|
||||
* :given-name="givenName"
|
||||
* message="Custom registration message here"
|
||||
* />
|
||||
* ```
|
||||
*
|
||||
* Component Dependencies:
|
||||
* - UserNameDialog: Dialog for entering user name
|
||||
* - ChoiceButtonDialog: Dialog for sharing method selection
|
||||
*/
|
||||
@Component({
|
||||
name: "RegistrationNotice",
|
||||
components: {
|
||||
UserNameDialog,
|
||||
ChoiceButtonDialog,
|
||||
},
|
||||
})
|
||||
export default class RegistrationNotice extends Vue {
|
||||
@Prop({ required: true }) isRegistered!: boolean;
|
||||
@Prop({ required: true }) show!: boolean;
|
||||
$router!: Router;
|
||||
|
||||
@Emit("share-info")
|
||||
shareInfo() {}
|
||||
/**
|
||||
* Whether passkeys are enabled in the application
|
||||
*/
|
||||
@Prop({ required: true })
|
||||
passkeysEnabled!: boolean;
|
||||
|
||||
/**
|
||||
* User's given name for dialog pre-population
|
||||
*/
|
||||
@Prop({ required: true })
|
||||
givenName!: string;
|
||||
|
||||
/**
|
||||
* Custom message to display in the registration notice
|
||||
* Defaults to "To share, someone must register you."
|
||||
*/
|
||||
@Prop({ default: "To share, someone must register you." })
|
||||
message!: string;
|
||||
|
||||
/**
|
||||
* Shows name input dialog if needed
|
||||
* Handles the full flow internally without requiring parent component intervention
|
||||
*/
|
||||
showNameThenIdDialog() {
|
||||
this.openUserNameDialog(() => {
|
||||
this.promptForShareMethod();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens advanced options page
|
||||
* Navigates directly to the start page
|
||||
*/
|
||||
openAdvancedOptions() {
|
||||
this.$router.push({ name: "start" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows dialog for sharing method selection
|
||||
* Provides options for different sharing scenarios
|
||||
*/
|
||||
promptForShareMethod() {
|
||||
(this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({
|
||||
title: "How can you share your info?",
|
||||
text: "",
|
||||
option1Text: "We are nearby with cameras",
|
||||
option2Text: "Someone created a meeting room",
|
||||
option3Text: "We will share some other way",
|
||||
onOption1: () => {
|
||||
this.handleQRCodeClick();
|
||||
},
|
||||
onOption2: () => {
|
||||
this.$router.push({ name: "onboard-meeting-list" });
|
||||
},
|
||||
onOption3: () => {
|
||||
this.$router.push({ name: "share-my-contact-info" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles QR code sharing based on platform
|
||||
* Navigates to appropriate QR code page
|
||||
*/
|
||||
private handleQRCodeClick() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the user name dialog if needed
|
||||
*
|
||||
* @param callback Function to call after name is entered
|
||||
*/
|
||||
openUserNameDialog(callback: () => void) {
|
||||
if (!this.givenName) {
|
||||
(this.$refs.userNameDialog as UserNameDialog).open(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -309,7 +309,7 @@ export function didInfoForContact(
|
||||
showDidForVisible: boolean = false,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
): { known: boolean; displayName: string; profileImageUrl?: string } {
|
||||
if (!did) return { displayName: "Someone Unnamed/Unknown", known: false };
|
||||
if (!did) return { displayName: "Someone Not Named", known: false };
|
||||
if (did === activeDid) {
|
||||
return { displayName: "You", known: true };
|
||||
} else if (contact) {
|
||||
|
||||
@@ -657,7 +657,7 @@ export async function saveNewIdentity(
|
||||
|
||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
||||
|
||||
await platformService.insertDidSpecificSettings(identity.did);
|
||||
await platformService.insertNewDidIntoSettings(identity.did);
|
||||
} catch (error) {
|
||||
logger.error("Failed to update default settings:", error);
|
||||
throw new Error(
|
||||
@@ -954,7 +954,7 @@ export async function importFromMnemonic(
|
||||
|
||||
try {
|
||||
// First, ensure the DID-specific settings record exists
|
||||
await platformService.insertDidSpecificSettings(newId.did);
|
||||
await platformService.insertNewDidIntoSettings(newId.did);
|
||||
|
||||
// Then update with Test User #0 specific settings
|
||||
await platformService.updateDidSpecificSettings(newId.did, {
|
||||
|
||||
@@ -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
26
src/main.ts
Normal 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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -175,11 +175,11 @@ export interface PlatformService {
|
||||
updateDefaultSettings(settings: Record<string, unknown>): Promise<void>;
|
||||
|
||||
/**
|
||||
* Inserts DID-specific settings into the database.
|
||||
* Inserts a new DID into the settings table.
|
||||
* @param did - The DID to associate with the settings
|
||||
* @returns Promise that resolves when the insertion is complete
|
||||
*/
|
||||
insertDidSpecificSettings(did: string): Promise<void>;
|
||||
insertNewDidIntoSettings(did: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates DID-specific settings in the database.
|
||||
|
||||
@@ -124,17 +124,55 @@ 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) {
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
logger.info("Profile deleted successfully");
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_DELETED);
|
||||
logger.error("Unexpected response status when deleting profile:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
});
|
||||
throw new Error(
|
||||
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.isApiError(error) && error.response) {
|
||||
const response = error.response;
|
||||
logger.error("API error deleting profile:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data,
|
||||
url: this.getErrorUrl(error),
|
||||
});
|
||||
|
||||
// Handle specific HTTP status codes
|
||||
if (response.status === 204) {
|
||||
logger.debug("Profile deleted successfully (204 No Content)");
|
||||
return true; // 204 is success for DELETE operations
|
||||
} else if (response.status === 404) {
|
||||
logger.warn("Profile not found - may already be deleted");
|
||||
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);
|
||||
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");
|
||||
} else if (response.status === 403) {
|
||||
logger.error("Forbidden to delete profile");
|
||||
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;
|
||||
@@ -204,13 +242,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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1319,7 +1319,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
async insertDidSpecificSettings(did: string): Promise<void> {
|
||||
async insertNewDidIntoSettings(did: string): Promise<void> {
|
||||
await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
|
||||
}
|
||||
|
||||
|
||||
@@ -681,7 +681,7 @@ export class WebPlatformService implements PlatformService {
|
||||
await this.dbExec(sql, params);
|
||||
}
|
||||
|
||||
async insertDidSpecificSettings(did: string): Promise<void> {
|
||||
async insertNewDidIntoSettings(did: string): Promise<void> {
|
||||
await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ export async function testServerRegisterUser() {
|
||||
"@/db/databaseUtil"
|
||||
);
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
const currentDid = settings?.activeDid;
|
||||
if (!currentDid) {
|
||||
throw new Error("No active DID found");
|
||||
}
|
||||
|
||||
// Make a claim
|
||||
const vcClaim = {
|
||||
@@ -57,7 +61,7 @@ export async function testServerRegisterUser() {
|
||||
"@type": "RegisterAction",
|
||||
agent: { identifier: identity0.did },
|
||||
object: SERVICE_ID,
|
||||
participant: { identifier: settings.activeDid },
|
||||
participant: { identifier: currentDid },
|
||||
};
|
||||
|
||||
// Make a payload for the claim
|
||||
@@ -94,5 +98,12 @@ export async function testServerRegisterUser() {
|
||||
|
||||
const resp = await axios.post(url, payload, { headers });
|
||||
logger.log("User registration result:", resp);
|
||||
|
||||
const platformService = await PlatformServiceFactory.getInstance();
|
||||
await platformService.updateDefaultSettings({ activeDid: currentDid });
|
||||
await platformService.updateDidSpecificSettings(currentDid!, {
|
||||
isRegistered: true,
|
||||
});
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
@@ -55,9 +55,10 @@
|
||||
|
||||
<!-- Registration notice -->
|
||||
<RegistrationNotice
|
||||
:is-registered="isRegistered"
|
||||
:show="showRegistrationNotice"
|
||||
@share-info="onShareInfo"
|
||||
v-if="!isRegistered"
|
||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||
:given-name="givenName"
|
||||
message="Before you can publicly announce a new project or time commitment, a friend needs to register you."
|
||||
/>
|
||||
|
||||
<!-- Notifications -->
|
||||
@@ -174,11 +175,12 @@
|
||||
:aria-busy="loadingProfile || savingProfile"
|
||||
></textarea>
|
||||
|
||||
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
|
||||
<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>
|
||||
@@ -194,6 +196,7 @@
|
||||
class="!z-40 rounded-md"
|
||||
@click="onProfileMapClick"
|
||||
@ready="onMapReady"
|
||||
@mounted="onMapMounted"
|
||||
>
|
||||
<l-tile-layer
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
@@ -751,6 +754,7 @@ import "dexie-export-import";
|
||||
// @ts-expect-error - they aren't exporting it but it's there
|
||||
import { ImportProgress } from "dexie-export-import";
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import * as L from "leaflet";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ref } from "vue";
|
||||
@@ -778,6 +782,7 @@ import {
|
||||
DEFAULT_PUSH_SERVER,
|
||||
IMAGE_TYPE_PROFILE,
|
||||
NotificationIface,
|
||||
PASSKEYS_ENABLED,
|
||||
} from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
@@ -848,6 +853,7 @@ export default class AccountViewView extends Vue {
|
||||
readonly DEFAULT_PUSH_SERVER: string = DEFAULT_PUSH_SERVER;
|
||||
readonly DEFAULT_IMAGE_API_SERVER: string = DEFAULT_IMAGE_API_SERVER;
|
||||
readonly DEFAULT_PARTNER_API_SERVER: string = DEFAULT_PARTNER_API_SERVER;
|
||||
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
|
||||
|
||||
// Identity and settings properties
|
||||
activeDid: string = "";
|
||||
@@ -902,6 +908,7 @@ export default class AccountViewView extends Vue {
|
||||
warnIfProdServer: boolean = false;
|
||||
warnIfTestServer: boolean = false;
|
||||
zoom: number = 2;
|
||||
isMapReady: boolean = false;
|
||||
|
||||
// Limits and validation properties
|
||||
endorserLimits: EndorserRateLimits | null = null;
|
||||
@@ -913,6 +920,23 @@ 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) {
|
||||
// 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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -939,10 +963,16 @@ 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
|
||||
}
|
||||
} else {
|
||||
// Profile not created yet; leave defaults
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error loading profile:", error);
|
||||
this.notify.error(
|
||||
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
|
||||
);
|
||||
@@ -1518,9 +1548,51 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
onMapReady(map: L.Map): void {
|
||||
// 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;
|
||||
map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom);
|
||||
try {
|
||||
logger.debug("Map ready event fired, map object:", map);
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
logger.debug("Map ready set to true after mounted");
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Fallback method to handle map initialization failures
|
||||
private handleMapInitFailure(): void {
|
||||
logger.debug("Starting map initialization timeout (5 seconds)");
|
||||
setTimeout(() => {
|
||||
if (!this.isMapReady) {
|
||||
logger.warn("Map failed to initialize, forcing ready state");
|
||||
this.isMapReady = true;
|
||||
} else {
|
||||
logger.debug("Map initialized successfully, timeout not needed");
|
||||
}
|
||||
}, 5000); // 5 second timeout
|
||||
}
|
||||
|
||||
showProfileInfo(): void {
|
||||
@@ -1532,13 +1604,16 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async saveProfile(): Promise<void> {
|
||||
this.savingProfile = true;
|
||||
const profileData: ProfileData = {
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
};
|
||||
try {
|
||||
const profileData: ProfileData = {
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
};
|
||||
|
||||
logger.debug("Saving profile data:", profileData);
|
||||
|
||||
const success = await this.profileService.saveProfile(
|
||||
this.activeDid,
|
||||
profileData,
|
||||
@@ -1549,6 +1624,7 @@ export default class AccountViewView extends Vue {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error saving profile:", error);
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
||||
} finally {
|
||||
this.savingProfile = false;
|
||||
@@ -1556,15 +1632,25 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
toggleUserProfileLocation(): void {
|
||||
const updated = this.profileService.toggleProfileLocation({
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
});
|
||||
this.userProfileLatitude = updated.latitude;
|
||||
this.userProfileLongitude = updated.longitude;
|
||||
this.includeUserProfileLocation = updated.includeLocation;
|
||||
try {
|
||||
const updated = this.profileService.toggleProfileLocation({
|
||||
description: this.userProfileDesc,
|
||||
latitude: this.userProfileLatitude,
|
||||
longitude: this.userProfileLongitude,
|
||||
includeLocation: this.includeUserProfileLocation,
|
||||
});
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in toggleUserProfileLocation:", error);
|
||||
this.notify.error("Failed to toggle location setting");
|
||||
}
|
||||
}
|
||||
|
||||
confirmEraseLatLong(): void {
|
||||
@@ -1592,6 +1678,7 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async deleteProfile(): Promise<void> {
|
||||
try {
|
||||
logger.debug("Attempting to delete profile for DID:", this.activeDid);
|
||||
const success = await this.profileService.deleteProfile(this.activeDid);
|
||||
if (success) {
|
||||
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
|
||||
@@ -1599,11 +1686,20 @@ export default class AccountViewView extends Vue {
|
||||
this.userProfileLatitude = 0;
|
||||
this.userProfileLongitude = 0;
|
||||
this.includeUserProfileLocation = false;
|
||||
this.isMapReady = false; // Reset map state
|
||||
logger.debug("Profile deleted successfully, UI state reset");
|
||||
} else {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
|
||||
}
|
||||
} catch (error) {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_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);
|
||||
} else {
|
||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1616,8 +1712,46 @@ export default class AccountViewView extends Vue {
|
||||
}
|
||||
|
||||
onProfileMapClick(event: LeafletMouseEvent) {
|
||||
this.userProfileLatitude = event.latlng.lat;
|
||||
this.userProfileLongitude = event.latlng.lng;
|
||||
try {
|
||||
if (event && event.latlng) {
|
||||
this.userProfileLatitude = event.latlng.lat;
|
||||
this.userProfileLongitude = event.latlng.lng;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in onProfileMapClick:", error);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
this.userProfileLatitude = 0;
|
||||
this.userProfileLongitude = 0;
|
||||
logger.debug("Location unchecked, map state reset");
|
||||
} 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) {
|
||||
logger.debug("Setting map ready after timeout");
|
||||
this.isMapReady = true;
|
||||
}
|
||||
}, 1000); // 1 second delay
|
||||
|
||||
this.handleMapInitFailure();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in onLocationCheckboxChange:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// IdentitySection event handlers
|
||||
@@ -1658,20 +1792,6 @@ export default class AccountViewView extends Vue {
|
||||
this.doCopyTwoSecRedo(did, () => (this.showDidCopy = !this.showDidCopy));
|
||||
}
|
||||
|
||||
get showRegistrationNotice(): boolean {
|
||||
// Show the notice if not registered and any other conditions you want
|
||||
return !this.isRegistered;
|
||||
}
|
||||
|
||||
onShareInfo() {
|
||||
// Navigate to QR code sharing page - mobile uses full scan, web uses basic
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
}
|
||||
|
||||
onRecheckLimits() {
|
||||
this.checkLimits();
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
icon="circle-question"
|
||||
class="text-slate-400 text-4xl"
|
||||
/>
|
||||
<span class="italic text-slate-400">(Unnamed/Unknown)</span>
|
||||
<span class="italic text-slate-400">(Not Named)</span>
|
||||
</span>
|
||||
<span class="text-right">
|
||||
<button
|
||||
|
||||
@@ -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];
|
||||
}),
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<!-- Heading -->
|
||||
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4 mb-8">
|
||||
Help
|
||||
<span class="text-xs text-gray-500">{{ package.version }}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -86,33 +86,14 @@ Raymer * @version 1.0.0 */
|
||||
Identity creation is now handled by router navigation guard.
|
||||
-->
|
||||
<div class="mb-4">
|
||||
<div
|
||||
<RegistrationNotice
|
||||
v-if="!isUserRegistered"
|
||||
id="noticeSomeoneMustRegisterYou"
|
||||
class="bg-amber-200 rounded-md overflow-hidden text-center px-4 py-3 mb-4"
|
||||
>
|
||||
To share, someone must register you.
|
||||
<div class="block text-center">
|
||||
<button
|
||||
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||
@click="showNameThenIdDialog()"
|
||||
>
|
||||
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
|
||||
info
|
||||
</button>
|
||||
</div>
|
||||
<UserNameDialog ref="userNameDialog" />
|
||||
<div class="flex justify-end w-full">
|
||||
<router-link
|
||||
:to="{ name: 'start' }"
|
||||
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||
>
|
||||
See advanced options
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||
:given-name="givenName"
|
||||
message="To share, someone must register you."
|
||||
/>
|
||||
|
||||
<div v-else id="sectionRecordSomethingGiven">
|
||||
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
|
||||
<!-- Record Quick-Action -->
|
||||
<div class="mb-6">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
@@ -252,8 +233,6 @@ Raymer * @version 1.0.0 */
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ChoiceButtonDialog ref="choiceButtonDialog" />
|
||||
|
||||
<ImageViewer v-model:is-open="isImageViewerOpen" :image-url="selectedImage" />
|
||||
</template>
|
||||
|
||||
@@ -261,7 +240,6 @@ Raymer * @version 1.0.0 */
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
|
||||
//import App from "../App.vue";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
@@ -272,10 +250,9 @@ import InfiniteScroll from "../components/InfiniteScroll.vue";
|
||||
import OnboardingDialog from "../components/OnboardingDialog.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
|
||||
import ImageViewer from "../components/ImageViewer.vue";
|
||||
import ActivityListItem from "../components/ActivityListItem.vue";
|
||||
import RegistrationNotice from "../components/RegistrationNotice.vue";
|
||||
import {
|
||||
AppString,
|
||||
NotificationIface,
|
||||
@@ -383,12 +360,11 @@ interface FeedError {
|
||||
GiftedPrompts,
|
||||
InfiniteScroll,
|
||||
OnboardingDialog,
|
||||
ChoiceButtonDialog,
|
||||
QuickNav,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
ImageViewer,
|
||||
ActivityListItem,
|
||||
RegistrationNotice,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
@@ -476,7 +452,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 +732,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 +838,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 +854,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 +909,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 +971,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 +981,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 +1151,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 +1602,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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1546,67 +1620,6 @@ export default class HomeView extends Vue {
|
||||
return known ? "text-slate-500" : "text-slate-100";
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows name input dialog if needed
|
||||
*
|
||||
* @public
|
||||
* @callGraph
|
||||
* Called by: Template
|
||||
* Calls:
|
||||
* - UserNameDialog.open()
|
||||
* - promptForShareMethod()
|
||||
*
|
||||
* @chain
|
||||
* Template -> showNameThenIdDialog() -> promptForShareMethod()
|
||||
*
|
||||
* @requires
|
||||
* - this.$refs.userNameDialog
|
||||
* - this.givenName
|
||||
*/
|
||||
showNameThenIdDialog() {
|
||||
if (!this.givenName) {
|
||||
(this.$refs.userNameDialog as UserNameDialog).open(() => {
|
||||
this.promptForShareMethod();
|
||||
});
|
||||
} else {
|
||||
this.promptForShareMethod();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows dialog for sharing method selection
|
||||
*
|
||||
* @internal
|
||||
* @callGraph
|
||||
* Called by: showNameThenIdDialog()
|
||||
* Calls: ChoiceButtonDialog.open()
|
||||
*
|
||||
* @chain
|
||||
* Template -> showNameThenIdDialog() -> promptForShareMethod()
|
||||
*
|
||||
* @requires
|
||||
* - this.$refs.choiceButtonDialog
|
||||
* - this.$router
|
||||
*/
|
||||
promptForShareMethod() {
|
||||
(this.$refs.choiceButtonDialog as ChoiceButtonDialog).open({
|
||||
title: "How can you share your info?",
|
||||
text: "",
|
||||
option1Text: "We are in a meeting together",
|
||||
option2Text: "We are nearby with cameras",
|
||||
option3Text: "We will share some other way",
|
||||
onOption1: () => {
|
||||
this.$router.push({ name: "onboard-meeting-list" });
|
||||
},
|
||||
onOption2: () => {
|
||||
this.handleQRCodeClick();
|
||||
},
|
||||
onOption3: () => {
|
||||
this.$router.push({ name: "share-my-contact-info" });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens image viewer dialog
|
||||
*
|
||||
@@ -1619,14 +1632,6 @@ export default class HomeView extends Vue {
|
||||
this.isImageViewerOpen = true;
|
||||
}
|
||||
|
||||
private handleQRCodeClick() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
}
|
||||
|
||||
openPersonDialog(
|
||||
giver?: GiverReceiverInputInfo | "Unnamed",
|
||||
prompt?: string,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -216,15 +216,12 @@
|
||||
<font-awesome icon="plus" :class="plusIconClasses" />
|
||||
button. You'll never know until you try.
|
||||
</div>
|
||||
<div v-else>
|
||||
<button
|
||||
:class="onboardingButtonClasses"
|
||||
@click="showNameThenIdDialog()"
|
||||
>
|
||||
Get someone to onboard you.
|
||||
</button>
|
||||
<UserNameDialog ref="userNameDialog" />
|
||||
</div>
|
||||
<RegistrationNotice
|
||||
v-else
|
||||
:passkeys-enabled="PASSKEYS_ENABLED"
|
||||
:given-name="givenName"
|
||||
message="To announce a project, get someone to onboard you."
|
||||
/>
|
||||
</div>
|
||||
<ul id="listProjects" class="border-t border-slate-300">
|
||||
<li
|
||||
@@ -266,14 +263,14 @@ import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
// Capacitor import removed - using QRNavigationService instead
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { NotificationIface, PASSKEYS_ENABLED } from "../constants/app";
|
||||
import EntityIcon from "../components/EntityIcon.vue";
|
||||
import InfiniteScroll from "../components/InfiniteScroll.vue";
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import OnboardingDialog from "../components/OnboardingDialog.vue";
|
||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import RegistrationNotice from "../components/RegistrationNotice.vue";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer";
|
||||
import { OfferSummaryRecord, PlanData } from "../interfaces/records";
|
||||
@@ -281,14 +278,13 @@ import { OnboardPage, iconForUnitCode } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { QRNavigationService } from "@/services/QRNavigationService";
|
||||
|
||||
import {
|
||||
NOTIFY_NO_ACCOUNT_ERROR,
|
||||
NOTIFY_PROJECT_LOAD_ERROR,
|
||||
NOTIFY_PROJECT_INIT_ERROR,
|
||||
NOTIFY_OFFERS_LOAD_ERROR,
|
||||
NOTIFY_OFFERS_FETCH_ERROR,
|
||||
NOTIFY_CAMERA_SHARE_METHOD,
|
||||
} from "@/constants/notifications";
|
||||
|
||||
/**
|
||||
@@ -318,7 +314,7 @@ import {
|
||||
OnboardingDialog,
|
||||
ProjectIcon,
|
||||
TopMessage,
|
||||
UserNameDialog,
|
||||
RegistrationNotice,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
@@ -336,6 +332,7 @@ export default class ProjectsView extends Vue {
|
||||
givenName = "";
|
||||
isLoading = false;
|
||||
isRegistered = false;
|
||||
readonly PASSKEYS_ENABLED: boolean = PASSKEYS_ENABLED;
|
||||
|
||||
// Data collections
|
||||
offers: OfferSummaryRecord[] = [];
|
||||
@@ -624,39 +621,6 @@ export default class ProjectsView extends Vue {
|
||||
* Ensures user has provided their name before proceeding with contact sharing.
|
||||
* Uses UserNameDialog component if name is not set.
|
||||
*/
|
||||
showNameThenIdDialog() {
|
||||
if (!this.givenName) {
|
||||
(this.$refs.userNameDialog as UserNameDialog).open(() => {
|
||||
this.promptForShareMethod();
|
||||
});
|
||||
} else {
|
||||
this.promptForShareMethod();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts user to choose contact sharing method
|
||||
*
|
||||
* Presents modal dialog asking if users are nearby with cameras.
|
||||
* Routes to appropriate sharing method based on user's choice:
|
||||
* - QR code sharing for nearby users with cameras
|
||||
* - Alternative sharing methods for remote users
|
||||
*/
|
||||
promptForShareMethod() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "modal",
|
||||
type: "confirm",
|
||||
title: NOTIFY_CAMERA_SHARE_METHOD.title,
|
||||
text: NOTIFY_CAMERA_SHARE_METHOD.text,
|
||||
onYes: () => this.handleQRCodeClick(),
|
||||
onNo: () => this.$router.push({ name: "share-my-contact-info" }),
|
||||
yesText: NOTIFY_CAMERA_SHARE_METHOD.yesText,
|
||||
noText: NOTIFY_CAMERA_SHARE_METHOD.noText,
|
||||
},
|
||||
TIMEOUTS.MODAL,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computed properties for template logic streamlining
|
||||
@@ -722,14 +686,6 @@ export default class ProjectsView extends Vue {
|
||||
return "bg-green-600 text-white px-1.5 py-1 rounded-full";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class names for onboarding button
|
||||
* @returns String with CSS classes for the onboarding button
|
||||
*/
|
||||
get onboardingButtonClasses() {
|
||||
return "text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class names for project tab styling
|
||||
* @returns Object with CSS classes based on current tab selection
|
||||
@@ -754,20 +710,6 @@ export default class ProjectsView extends Vue {
|
||||
* Utility methods
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handles QR code sharing functionality with platform detection
|
||||
*
|
||||
* Routes to appropriate QR code interface based on current platform:
|
||||
* - Full QR scanner for native mobile platforms
|
||||
* - Web-based QR interface for browser environments
|
||||
*/
|
||||
private handleQRCodeClick() {
|
||||
const qrNavigationService = QRNavigationService.getInstance();
|
||||
const route = qrNavigationService.getQRScannerRoute();
|
||||
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy method compatibility
|
||||
* @deprecated Use computedOfferTabClassNames for backward compatibility
|
||||
|
||||
Reference in New Issue
Block a user