Merge branch 'master' into nearby-filter

This commit is contained in:
Matthew Raymer
2025-08-15 07:22:28 +00:00
38 changed files with 451 additions and 221 deletions

View File

@@ -27,9 +27,13 @@
v-if="notification.type === 'toast'"
class="w-full max-w-sm mx-auto mb-3 overflow-hidden bg-slate-900/90 text-white rounded-lg shadow-md"
>
<div class="w-full px-4 py-3">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ notification.text }}</p>
<div class="w-full px-4 py-3 overflow-hidden">
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
</div>
</div>
@@ -46,9 +50,15 @@
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<div
class="relative w-full pl-4 pr-8 py-2 text-slate-900 overflow-hidden"
>
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
@@ -72,9 +82,15 @@
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<div
class="relative w-full pl-4 pr-8 py-2 text-emerald-900 overflow-hidden"
>
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
@@ -98,9 +114,15 @@
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<div
class="relative w-full pl-4 pr-8 py-2 text-amber-900 overflow-hidden"
>
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
@@ -124,9 +146,15 @@
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
<span class="font-semibold">{{ notification.title }}</span>
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<div
class="relative w-full pl-4 pr-8 py-2 text-rose-900 overflow-hidden"
>
<h4 class="font-semibold text-ellipsis overflow-hidden">
{{ notification.title }}
</h4>
<p class="text-sm text-ellipsis overflow-hidden">
{{ notification.text }}
</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
@@ -349,13 +377,6 @@ export default class App extends Vue {
stopAsking = false;
truncateLongWords(sentence: string) {
return sentence
.split(" ")
.map((word) => (word.length > 30 ? word.slice(0, 30) + "..." : word))
.join(" ");
}
async turnOffNotifications(
notification: NotificationIface,
): Promise<boolean> {

View File

@@ -73,7 +73,6 @@
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image"
alt="Activity image"
@load="handleImageLoad(record.image)"
/>
</a>
</div>
@@ -253,7 +252,7 @@ import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue";
import { isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers } from "@/utils/notify";
import { createNotifyHelpers, NotifyFunction } from "@/utils/notify";
import {
NOTIFY_PERSON_HIDDEN,
NOTIFY_UNKNOWN_PERSON,
@@ -272,16 +271,9 @@ export default class ActivityListItem extends Vue {
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
/**
* Function prop for handling image caching
* Called when an image loads successfully, allowing parent to control caching behavior
*/
@Prop({ type: Function, default: () => {} })
onImageCache!: (imageUrl: string) => void | Promise<void>;
isHiddenDid = isHiddenDid;
notify!: ReturnType<typeof createNotifyHelpers>;
$notify!: (notification: any, timeout?: number) => void;
$notify!: NotifyFunction;
created() {
this.notify = createNotifyHelpers(this.$notify);
@@ -295,14 +287,6 @@ export default class ActivityListItem extends Vue {
this.notify.warning(NOTIFY_UNKNOWN_PERSON.message, TIMEOUTS.STANDARD);
}
/**
* Handle image load event - call function prop for caching
* Allows parent to control caching behavior and validation
*/
handleImageLoad(imageUrl: string): void {
this.onImageCache(imageUrl);
}
get fetchAmount(): string {
const claim =
(this.record.fullClaim as any)?.claim || this.record.fullClaim;

View File

@@ -167,7 +167,7 @@ export default class ContactInputForm extends Vue {
*/
@Emit("qr-scan")
private handleQRScan(): void {
console.log("[ContactInputForm] QR scan button clicked");
// QR scan button clicked - event emitted for parent handling
}
}
</script>

View File

@@ -80,7 +80,7 @@ import EntitySelectionStep from "../components/EntitySelectionStep.vue";
import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
@@ -98,7 +98,7 @@ import {
mixins: [PlatformServiceMixin],
})
export default class GiftedDialog extends Vue {
$notify!: (notification: any, timeout?: number) => void;
$notify!: NotifyFunction;
notify!: ReturnType<typeof createNotifyHelpers>;
/**

View File

@@ -282,7 +282,7 @@ import {
NOTIFY_IMAGE_DIALOG_UNSUPPORTED_FORMAT,
createImageDialogCameraErrorMessage,
} from "../constants/notifications";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "../utils/notify";
const inputImageFileNameRef = ref<Blob>();
@@ -291,7 +291,7 @@ const inputImageFileNameRef = ref<Blob>();
mixins: [PlatformServiceMixin],
})
export default class ImageMethodDialog extends Vue {
$notify!: (notification: any, timeout?: number) => void;
$notify!: NotifyFunction;
$router!: Router;
notify = createNotifyHelpers(this.$notify);

View File

@@ -45,7 +45,6 @@ import { logger } from "../utils/logger";
@Component({ emits: ["update:isOpen"] })
export default class ImageViewer extends Vue {
@Prop() imageUrl!: string;
@Prop() imageData!: Blob | null;
@Prop() isOpen!: boolean;
userAgent = new UAParser();

View File

@@ -1348,12 +1348,12 @@ export async function createEndorserJwtVcFromClaim(
}
/**
* Create a JWT for a RegisterAction claim.
* Create a JWT for a RegisterAction claim, used for registrations & invites.
*
* @param activeDid - The DID of the user creating the invite
* @param contact - The contact to register, with a 'did' field (all optional for invites)
* @param identifier - The identifier for the invite, usually random
* @param expiresIn - The number of seconds until the invite expires
* @param contact - Optional - The contact to register, with a 'did' field (all optional for invites)
* @param identifier - Optional - The identifier for the invite, usually random
* @param expiresIn - Optional - The number of seconds until the invite expires
* @returns The JWT for the RegisterAction claim
*/
export async function createInviteJwt(
@@ -1367,7 +1367,7 @@ export async function createInviteJwt(
"@type": "RegisterAction",
agent: { identifier: activeDid },
object: SERVICE_ID,
identifier: identifier,
identifier: identifier, // not sent if undefined
};
if (contact?.did) {
vcClaim.participant = { identifier: contact.did };

View File

@@ -82,6 +82,15 @@ const routes: Array<RouteRecordRaw> = [
name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"),
},
{
path: "/deep-link-error",
name: "deep-link-error",
component: () => import("../views/DeepLinkErrorView.vue"),
meta: {
title: "Invalid Deep Link",
requiresAuth: false,
},
},
{
path: "/did/:did?",
name: "did",
@@ -276,15 +285,6 @@ const routes: Array<RouteRecordRaw> = [
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:";

View File

@@ -1,7 +1,9 @@
// **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top
// This prevents "crypto is not defined" errors when running in worker context
if (typeof window === "undefined" && typeof crypto === "undefined") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).crypto = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getRandomValues: (array: any) => {
// Simple fallback for worker context
for (let i = 0; i < array.length; i++) {

View File

@@ -53,6 +53,7 @@ import {
DeepLinkRoute,
} from "../interfaces/deepLinks";
import type { DeepLinkError } from "../interfaces/deepLinks";
import { logger } from "../utils/logger";
// Helper function to extract the first key from a Zod object schema
function getFirstKeyFromZodObject(
@@ -178,7 +179,7 @@ export class DeepLinkHandler {
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = ROUTE_MAP[validRoute].name;
} catch (error) {
console.error(`[DeepLink] Invalid route path: ${path}`);
logger.error(`[DeepLink] Invalid route path: ${path}`);
// Redirect to error page with information about the invalid link
await this.router.replace({
@@ -204,9 +205,8 @@ export class DeepLinkHandler {
validatedParams = await schema.parseAsync(params);
} catch (error) {
// For parameter validation errors, provide specific error feedback
logConsoleAndDb(
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)}`,
true,
);
await this.router.replace({
name: "deep-link-error",
@@ -229,9 +229,8 @@ export class DeepLinkHandler {
params: validatedParams,
});
} catch (error) {
logConsoleAndDb(
logger.error(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`,
true,
);
// For parameter validation errors, provide specific error feedback
await this.router.replace({
@@ -263,9 +262,8 @@ export class DeepLinkHandler {
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logConsoleAndDb(
logger.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
true,
);
throw {

View File

@@ -693,7 +693,8 @@ export class WebPlatformService implements PlatformService {
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did];
console.log(
// Log update operation for debugging
logger.debug(
"[WebPlatformService] updateDidSpecificSettings",
sql,
JSON.stringify(params, null, 2),

View File

@@ -92,6 +92,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
})
export default class PlatformServiceMixinTest extends Vue {
result: string = "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
userZeroTestResult: any = null;
activeTest: string = ""; // Track which test is currently active
@@ -267,6 +268,7 @@ This tests the complete save → retrieve cycle with actual database interaction
this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`;
} catch (error) {
this.result = `Error testing User #0 settings: ${error}`;
// eslint-disable-next-line no-console
console.error("Error testing User #0 settings:", error);
}
}

View File

@@ -1,9 +1,34 @@
import axios from "axios";
import * as didJwt from "did-jwt";
import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto";
import {
DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress,
newIdentifier,
} from "../libs/crypto";
import { logger } from "../utils/logger";
import { AppString } from "../constants/app";
import { saveNewIdentity } from "@/libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
const TEST_USER_0_MNEMONIC =
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
export async function testBecomeUser0() {
const [addr, privateHex, publicHex, deriPath] =
deriveAddress(TEST_USER_0_MNEMONIC);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
await saveNewIdentity(
identity0,
TEST_USER_0_MNEMONIC,
DEFAULT_ROOT_DERIVATION_PATH,
);
const platformService = await PlatformServiceFactory.getInstance();
await platformService.updateDidSpecificSettings(identity0.did, {
isRegistered: true,
});
}
/**
* Get User #0 to sign & submit a RegisterAction for the user's activeDid.
@@ -15,10 +40,8 @@ import { AppString } from "../constants/app";
* @throws Error if registration fails or database access fails
*/
export async function testServerRegisterUser() {
const testUser0Mnem =
"seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control";
const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem);
const [addr, privateHex, publicHex, deriPath] =
deriveAddress(TEST_USER_0_MNEMONIC);
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
@@ -32,9 +55,9 @@ export async function testServerRegisterUser() {
const vcClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { did: identity0.did },
agent: { identifier: identity0.did },
object: SERVICE_ID,
participant: { did: settings.activeDid },
participant: { identifier: settings.activeDid },
};
// Make a payload for the claim
@@ -71,4 +94,5 @@ export async function testServerRegisterUser() {
const resp = await axios.post(url, payload, { headers });
logger.log("User registration result:", resp);
return resp;
}

View File

@@ -133,6 +133,7 @@ export const PlatformServiceMixin = {
* Used for change detection and component updates
*/
currentActiveDid(): string | null {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (this as any)._currentActiveDid;
},
@@ -200,7 +201,9 @@ export const PlatformServiceMixin = {
* This method should be called when the user switches identities
*/
async $updateActiveDid(newDid: string | null): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const oldDid = (this as any)._currentActiveDid;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(this as any)._currentActiveDid = newDid;
if (newDid !== oldDid) {
@@ -291,6 +294,7 @@ export const PlatformServiceMixin = {
// Convert searchBoxes array to JSON string if present
if (settings.searchBoxes !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(converted as any).searchBoxes = Array.isArray(settings.searchBoxes)
? JSON.stringify(settings.searchBoxes)
: String(settings.searchBoxes);
@@ -692,6 +696,7 @@ export const PlatformServiceMixin = {
typeof method.value === "string";
if (!isValid && method !== undefined) {
// eslint-disable-next-line no-console
console.warn(
"[ContactNormalization] Invalid contact method:",
method,

View File

@@ -2,9 +2,10 @@
* Enhanced logger with self-contained database logging
*
* Provides comprehensive logging with console and database output.
* Supports configurable log levels via VITE_LOG_LEVEL environment variable.
*
* @author Matthew Raymer
* @version 2.0.0
* @version 2.1.0
* @since 2025-01-25
*/
@@ -46,6 +47,42 @@ export function safeStringify(obj: unknown) {
const isElectron = process.env.VITE_PLATFORM === "electron";
const isProduction = process.env.NODE_ENV === "production";
// Log level configuration via environment variable
const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
} as const;
type LogLevel = keyof typeof LOG_LEVELS;
// Parse VITE_LOG_LEVEL environment variable
const getLogLevel = (): LogLevel => {
const envLogLevel = process.env.VITE_LOG_LEVEL?.toLowerCase();
if (envLogLevel && envLogLevel in LOG_LEVELS) {
return envLogLevel as LogLevel;
}
// Default log levels based on environment
if (isProduction && !isElectron) {
return "warn"; // Production web: warnings and errors only
} else if (isElectron) {
return "error"; // Electron: errors only
} else {
return "info"; // Development/Capacitor: info and above
}
};
const currentLogLevel = getLogLevel();
const currentLevelValue = LOG_LEVELS[currentLogLevel];
// Helper function to check if a log level should be displayed
const shouldLog = (level: LogLevel): boolean => {
return LOG_LEVELS[level] <= currentLevelValue;
};
// Track initialization state to prevent circular dependencies
let isInitializing = true;
@@ -105,11 +142,11 @@ async function logToDatabase(
}
}
// Enhanced logger with self-contained database methods
// Enhanced logger with self-contained database methods and log level control
export const logger = {
debug: (message: string, ...args: unknown[]) => {
// Debug logs are very verbose - only show in development mode for web
if (!isProduction && !isElectron) {
// Debug logs only show if VITE_LOG_LEVEL allows it
if (shouldLog("debug")) {
// eslint-disable-next-line no-console
console.debug(message, ...args);
}
@@ -117,11 +154,8 @@ export const logger = {
},
log: (message: string, ...args: unknown[]) => {
// Regular logs - show in development or for capacitor, but quiet for Electron
if (
(!isProduction && !isElectron) ||
process.env.VITE_PLATFORM === "capacitor"
) {
// Regular logs - show if VITE_LOG_LEVEL allows info level
if (shouldLog("info")) {
// eslint-disable-next-line no-console
console.log(message, ...args);
}
@@ -132,11 +166,7 @@ export const logger = {
},
info: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor" ||
process.env.VITE_PLATFORM === "electron"
) {
if (shouldLog("info")) {
// eslint-disable-next-line no-console
console.info(message, ...args);
}
@@ -147,8 +177,7 @@ export const logger = {
},
warn: (message: string, ...args: unknown[]) => {
// Always show warnings, but for Electron, suppress routine database warnings
if (!isElectron || !message.includes("[CapacitorPlatformService]")) {
if (shouldLog("warn")) {
// eslint-disable-next-line no-console
console.warn(message, ...args);
}
@@ -159,9 +188,10 @@ export const logger = {
},
error: (message: string, ...args: unknown[]) => {
// Errors will always be logged to console
// eslint-disable-next-line no-console
console.error(message, ...args);
if (shouldLog("error")) {
// eslint-disable-next-line no-console
console.error(message, ...args);
}
// Database logging
const messageString = safeStringify(message);
@@ -175,11 +205,11 @@ export const logger = {
},
toConsoleAndDb: async (message: string, isError = false): Promise<void> => {
// Console output
if (isError) {
// Console output based on log level
if (isError && shouldLog("error")) {
// eslint-disable-next-line no-console
console.error(message);
} else {
} else if (!isError && shouldLog("info")) {
// eslint-disable-next-line no-console
console.log(message);
}
@@ -194,6 +224,12 @@ export const logger = {
error: (message: string) =>
logToDatabase(`[${componentName}] ${message}`, "error"),
}),
// Log level information methods
getCurrentLevel: (): LogLevel => currentLogLevel,
getCurrentLevelValue: (): number => currentLevelValue,
isLevelEnabled: (level: LogLevel): boolean => shouldLog(level),
getAvailableLevels: (): LogLevel[] => Object.keys(LOG_LEVELS) as LogLevel[],
};
// Function to manually mark initialization as complete

View File

@@ -61,7 +61,8 @@
/>
<!-- Notifications -->
<!-- Currently disabled because it doesn't work, even on Chrome. If restored, make sure it works or doesn't show on mobile/electron. -->
<!-- Currently disabled because it doesn't work, even on Chrome.
If restored, make sure it works or doesn't show on mobile/electron. -->
<section
v-if="false"
id="sectionNotifications"

View File

@@ -724,6 +724,8 @@ export default class ClaimView extends Vue {
}
async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
@@ -754,8 +756,6 @@ export default class ClaimView extends Vue {
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
this.canShare = !!navigator.share;
this.notify = createNotifyHelpers(this.$notify);
}
// insert a space before any capital letters except the initial letter

View File

@@ -143,7 +143,7 @@ import {
QR_TIMEOUT_STANDARD,
QR_TIMEOUT_LONG,
} from "@/constants/notifications";
import { createNotifyHelpers } from "../utils/notify";
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
interface QRScanResult {
rawValue?: string;
@@ -191,7 +191,7 @@ interface IUserNameDialog {
* @since 2024
*/
export default class ContactQRScanFull extends Vue {
$notify!: (notification: any, timeout?: number) => void;
$notify!: NotifyFunction;
$router!: Router;
// Notification helper system

View File

@@ -7,7 +7,8 @@
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
<!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page (and going back there is annoying). -->
<!-- Go to 'contacts' instead of just 'back' because they could get here from an edit page
(and going back there is annoying). -->
<router-link
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"

View File

@@ -234,7 +234,6 @@ Raymer * @version 1.0.0 */
:last-viewed-claim-id="feedLastViewedClaimId"
:is-registered="isRegistered"
:active-did="activeDid"
:on-image-cache="cacheImageData"
@load-claim="onClickLoadClaim"
@view-image="openImageViewer"
/>
@@ -255,11 +254,7 @@ Raymer * @version 1.0.0 */
<ChoiceButtonDialog ref="choiceButtonDialog" />
<ImageViewer
v-model:is-open="isImageViewerOpen"
:image-url="selectedImage"
:image-data="selectedImageData"
/>
<ImageViewer v-model:is-open="isImageViewerOpen" :image-url="selectedImage" />
</template>
<script lang="ts">
@@ -434,9 +429,7 @@ export default class HomeView extends Vue {
showShortcutBvc = false;
userAgentInfo = new UAParser(); // see https://docs.uaparser.js.org/v2/api/ua-parser-js/get-os.html
selectedImage = "";
selectedImageData: Blob | null = null;
isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false;
/**
@@ -1712,23 +1705,6 @@ export default class HomeView extends Vue {
});
}
/**
* Caches image data for sharing
*
* @public
* Called by ActivityListItem component function prop
* @param imageUrl URL of image to cache
*/
async cacheImageData(imageUrl: string) {
try {
// For images that might fail CORS, just store the URL
// The Web Share API will handle sharing the URL appropriately
this.imageCache.set(imageUrl, null);
} catch (error) {
logger.warn("Failed to cache image:", error);
}
}
/**
* Opens image viewer dialog
*
@@ -1737,7 +1713,6 @@ export default class HomeView extends Vue {
* @param imageUrl URL of image to display
*/
async openImageViewer(imageUrl: string) {
this.selectedImageData = this.imageCache.get(imageUrl) ?? null;
this.selectedImage = imageUrl;
this.isImageViewerOpen = true;
}

View File

@@ -87,7 +87,7 @@ import {
import { logger } from "../utils/logger";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import {
NOTIFY_ACCOUNT_DERIVATION_SUCCESS,
NOTIFY_ACCOUNT_DERIVATION_ERROR,
@@ -100,7 +100,7 @@ import {
export default class ImportAccountView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
$notify!: (notification: any, timeout?: number) => void;
$notify!: NotifyFunction;
notify!: ReturnType<typeof createNotifyHelpers>;

View File

@@ -21,7 +21,17 @@
</h1>
</div>
<div>
<div v-if="isNotProdServer">
<h2 class="text-xl font-bold mb-4">User Registration</h2>
<button :class="primaryButtonClasses" @click="registerMe()">
Register Yourself
</button>
<button :class="primaryButtonClasses" @click="becomeUser0()">
Become User 0 (who can register others)
</button>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Notiwind Alerts</h2>
<!-- Notification test buttons using computed configuration -->
@@ -99,7 +109,7 @@
<div>
Register Passkey
<button :class="primaryButtonClasses" @click="register()">
<button :class="primaryButtonClasses" @click="registerPasskey()">
Simplewebauthn
</button>
</div>
@@ -235,6 +245,7 @@ import {
registerAndSavePasskey,
SHARED_PHOTO_BASE64_KEY,
} from "../libs/util";
import { testBecomeUser0, testServerRegisterUser } from "@/test";
import { logger } from "../utils/logger";
import { Account } from "../db/tables/accounts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
@@ -300,6 +311,7 @@ export default class Help extends Vue {
// for passkeys
credIdHex?: string;
activeDid?: string;
apiServer?: string;
jwt?: string;
peerSetup?: PeerSetup;
userName?: string;
@@ -521,17 +533,6 @@ export default class Help extends Vue {
];
}
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/**
* Component initialization
*
@@ -541,6 +542,7 @@ export default class Help extends Vue {
async mounted() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName;
const account = await retrieveAccountMetadata(this.activeDid);
@@ -553,6 +555,43 @@ export default class Help extends Vue {
}
}
/**
* Checks if running on production server
*
* @returns True if not on production server (enables test utilities)
*/
public isNotProdServer() {
return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER;
}
async registerMe() {
const response = await testServerRegisterUser();
if (response.status === 201) {
alert("Registration successful.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
} else {
logger.error("Registration failure response:", response);
alert("Registration failed: " + (response.data.error || response.data));
}
}
async becomeUser0() {
await testBecomeUser0();
alert("You are now User 0.");
this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change
}
/**
* Method to trigger notification test
* Centralizes notification testing logic
*/
triggerTestNotification(config: {
notification: NotificationIface;
timeout?: number;
}) {
this.$notify(config.notification, config.timeout);
}
/**
* Handles file upload for image sharing tests
*
@@ -609,7 +648,7 @@ export default class Help extends Vue {
* Includes validation and user confirmation workflow
* Uses notification helpers for consistent messaging
*/
public async register() {
public async registerPasskey() {
const DEFAULT_USERNAME = AppString.APP_NAME + " Tester";
if (!this.userName) {
const modalConfig = createPasskeyNameModal(