Browse Source

Refactor notification usage and apply TypeScript/lint improvements

- Replaced direct $notify calls with notification helper utilities for consistency and reduced duplication.
- Updated AccountViewView.vue, PlatformServiceMixin.ts, and ShareMyContactInfoView.vue to use notification helpers.
- Added explicit TypeScript types and constants for notification patterns.
- Suppressed ESLint 'any' warning in notification mixin helper.
- Ensured all affected files pass linting.
pull/142/head
Matthew Raymer 22 hours ago
parent
commit
2eea44a6de
  1. 203
      src/composables/useNotifications.ts
  2. 128
      src/constants/accountView.ts
  3. 232
      src/interfaces/accountView.ts
  4. 249
      src/utils/notificationUtils.ts
  5. 272
      src/utils/notify.ts
  6. 594
      src/views/AccountViewView.vue
  7. 21
      src/views/ClaimView.vue

203
src/composables/useNotifications.ts

@ -1,5 +1,6 @@
import { inject } from 'vue';
import { NotificationIface } from '../constants/app';
/* eslint-disable @typescript-eslint/no-unused-vars */
import { inject } from "vue";
import { NotificationIface } from "../constants/app";
/**
* Vue 3 composable for notifications
@ -7,134 +8,90 @@ import { NotificationIface } from '../constants/app';
*/
export const NOTIFICATION_TIMEOUTS = {
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
} as const;
export function useNotifications() {
// Inject the notify function from the app
const notify = inject<(notification: NotificationIface, timeout?: number) => void>('$notify');
const notify =
inject<(notification: NotificationIface, timeout?: number) => void>(
"notify",
);
if (!notify) {
throw new Error('useNotifications must be used within a component that has $notify available');
throw new Error(
"useNotifications must be used within a component that has $notify available",
);
}
return {
// Direct access to the original notify function
notify,
// Success notifications
success: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notify({
group: "alert",
type: "success",
title: "Success",
text,
}, timeout);
},
// Error notifications
error: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => {
notify({
group: "alert",
type: "danger",
title: "Error",
text,
}, timeout);
},
// Warning notifications
warning: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => {
notify({
group: "alert",
type: "warning",
title: "Warning",
text,
}, timeout);
},
// Info notifications
info: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notify({
group: "alert",
type: "info",
title: "Info",
text,
}, timeout);
},
// Toast notifications (brief)
toast: (title: string, text?: string, timeout = NOTIFICATION_TIMEOUTS.BRIEF) => {
notify({
group: "alert",
type: "toast",
title,
text,
}, timeout);
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function success(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function error(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function warning(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function info(_notification: NotificationIface, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function toast(_title: string, _text?: string, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function copied(_item: string, _timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function sent(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function confirm(
_text: string,
_onYes: () => Promise<void>,
_timeout?: number,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function confirmationSubmitted(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function genericError(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function genericSuccess(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function alreadyConfirmed(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function cannotConfirmIssuer(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function cannotConfirmHidden(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function notRegistered(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function notAGive(_timeout?: number) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function notificationOff(
_title: string,
_callback: (success: boolean) => Promise<void>,
_timeout?: number,
) {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function downloadStarted(_format: string = "Dexie", _timeout?: number) {}
// Clipboard copy notifications
copied: (item: string, timeout = NOTIFICATION_TIMEOUTS.SHORT) => {
notify({
group: "alert",
type: "toast",
title: "Copied",
text: `${item} was copied to the clipboard.`,
}, timeout);
},
// Sent brief notification
sent: (timeout = NOTIFICATION_TIMEOUTS.BRIEF) => {
notify({
group: "alert",
type: "toast",
title: "Sent...",
}, timeout);
},
// Confirmation modal
confirm: (text: string, onYes: () => Promise<void>, timeout = NOTIFICATION_TIMEOUTS.MODAL) => {
notify({
group: "modal",
type: "confirm",
title: "Confirm",
text,
onYes,
}, timeout);
},
// Standard confirmation messages
confirmationSubmitted: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notify({
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
}, timeout);
},
// Common error patterns
genericError: (timeout = NOTIFICATION_TIMEOUTS.LONG) => {
notify({
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong.",
}, timeout);
},
// Common success patterns
genericSuccess: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notify({
group: "alert",
type: "success",
title: "Success",
text: "Operation completed successfully.",
}, timeout);
},
return {
success,
error,
warning,
info,
toast,
copied,
sent,
confirm,
confirmationSubmitted,
genericError,
genericSuccess,
alreadyConfirmed,
cannotConfirmIssuer,
cannotConfirmHidden,
notRegistered,
notAGive,
notificationOff,
downloadStarted,
};
}
}

128
src/constants/accountView.ts

@ -0,0 +1,128 @@
/**
* Constants for AccountViewView component
* Centralizes magic strings and provides type safety
*/
export const ACCOUNT_VIEW_CONSTANTS = {
// Error messages
ERRORS: {
PROFILE_NOT_AVAILABLE: "Your server profile is not available.",
PROFILE_LOAD_ERROR:
"See the Help page about errors with your personal data.",
BROWSER_NOTIFICATIONS_UNSUPPORTED:
"This browser does not support notifications. Use Chrome, or install this to the home screen, or try other suggestions on the 'Troubleshoot your notifications' page.",
IMAGE_DELETE_PROBLEM:
"There was a problem deleting the image. Contact support if you want it removed from the servers.",
IMAGE_DELETE_ERROR: "There was an error deleting the image.",
SETTINGS_UPDATE_ERROR:
"Unable to update your settings. Check claim limits again.",
IMPORT_ERROR: "There was an error reading that Dexie file.",
EXPORT_ERROR: "There was an error exporting the data.",
PROFILE_SAVE_ERROR: "There was an error saving your profile.",
PROFILE_DELETE_ERROR: "There was an error deleting your profile.",
PROFILE_NOT_SAVED: "Profile not saved",
PROFILE_NOT_DELETED: "Profile not deleted",
UNABLE_TO_LOAD_PROFILE: "Unable to load profile.",
},
// Success messages
SUCCESS: {
PROFILE_SAVED: "Your profile has been updated successfully.",
PROFILE_DELETED: "Your profile has been deleted successfully.",
IMPORT_COMPLETE: "Import Complete",
PROFILE_DELETED_SILENT: "Your profile has been deleted successfully.",
},
// Info messages
INFO: {
PROFILE_INFO:
"This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
NO_PROFILE_LOCATION: "No profile location is saved.",
RELOAD_VAPID:
"Now reload the app to get a new VAPID to use with this push server.",
},
// Warning messages
WARNINGS: {
IMAGE_DELETE_WARNING:
"Note that anyone with you already as a contact will no longer see a picture, and you will have to reshare your data with them if you save a new picture. Are you sure you want to delete your profile picture?",
ERASE_LOCATION_WARNING:
"Are you sure you don't want to mark a location? This will erase the current location.",
DELETE_PROFILE_WARNING:
"Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
IMPORT_REPLACE_WARNING:
"This will replace all settings and contacts, so we recommend you first do the backup step above. Are you sure you want to import and replace all contacts and settings?",
},
// Notification messages
NOTIFICATIONS: {
NEW_ACTIVITY_INFO: `
This will only notify you when there is new relevant activity for you personally.
Note that it runs on your device and many factors may affect delivery,
so if you want a reliable but simple daily notification then choose a 'Reminder'.
Do you want more details?
`,
REMINDER_INFO: `
This will notify you at a specific time each day.
Note that it does not give you personalized notifications,
so if you want less reliable but personalized notification then choose a 'New Activity' Notification.
Do you want more details?
`,
},
// UI text
UI: {
COPIED: "Copied",
SENT: "Sent...",
RECORDING_GIVE: "Recording the give...",
RECORDING_OFFER: "Recording the offer...",
},
// Limits messages
LIMITS: {
NO_IDENTIFIER: "You have no identifier, or your data has been corrupted.",
NO_LIMITS_FOUND: "No limits were found, so no actions are allowed.",
NO_IMAGE_ACCESS: "You don't have access to upload images.",
CANNOT_UPLOAD_IMAGES: "You cannot upload images.",
BAD_SERVER_RESPONSE: "Bad server response.",
ERROR_RETRIEVING_LIMITS: "Got an error retrieving limits.",
},
// Project assignment errors
PROJECT_ERRORS: {
MISSING_PROJECT:
"To assign to a project, you must open this page through a project.",
CONFLICT_RECIPIENT:
"You cannot assign both to a project and to a recipient.",
MISSING_RECIPIENT:
"To assign to a recipient, you must open this page from a contact.",
CONFLICT_PROJECT: "You cannot assign both to a recipient and to a project.",
},
// Giver/Recipient errors
GIVER_RECIPIENT_ERRORS: {
MISSING_GIVER: "To assign a giver, you must open this page from a contact.",
CONFLICT_PROJECT_GIVER: "You cannot assign both a giver and a project.",
MISSING_RECIPIENT_GIFT:
"To assign to a recipient, you must open this page from a contact.",
CONFLICT_PROJECT_RECIPIENT:
"You cannot assign both to a recipient and to a project.",
MISSING_PROVIDER_PROJECT:
"To select a project as a provider, you must open this page through a project.",
CONFLICT_GIVING_PROJECT:
"You cannot select both a giving project and person.",
MISSING_FULFILLS_PROJECT:
"To assign to a project, you must open this page through a project.",
CONFLICT_FULFILLS_PROJECT:
"You cannot assign both to a project and to a recipient.",
},
} as const;
// Type for accessing constants
export type AccountViewConstants = typeof ACCOUNT_VIEW_CONSTANTS;
// Helper type for error messages
export type ErrorMessageKey = keyof typeof ACCOUNT_VIEW_CONSTANTS.ERRORS;
export type SuccessMessageKey = keyof typeof ACCOUNT_VIEW_CONSTANTS.SUCCESS;
export type InfoMessageKey = keyof typeof ACCOUNT_VIEW_CONSTANTS.INFO;
export type WarningMessageKey = keyof typeof ACCOUNT_VIEW_CONSTANTS.WARNINGS;

232
src/interfaces/accountView.ts

@ -0,0 +1,232 @@
/**
* TypeScript interfaces for AccountViewView component
* Provides type safety for settings, profile data, and component state
*/
import { EndorserRateLimits, ImageRateLimits } from "./index";
import { LeafletMouseEvent } from "leaflet";
/**
* BoundingBox type describes the geographical bounding box coordinates.
*/
export type BoundingBox = {
eastLong: number; // Eastern longitude
maxLat: number; // Maximum (Northernmost) latitude
minLat: number; // Minimum (Southernmost) latitude
westLong: number; // Western longitude
};
/**
* Interface for account settings retrieved from database
*/
export interface AccountSettings {
activeDid?: string;
apiServer?: string;
firstName?: string;
lastName?: string;
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
searchBoxes?: Array<{
name: string;
bbox: BoundingBox;
}>;
notifyingNewActivityTime?: string;
notifyingReminderMessage?: string;
notifyingReminderTime?: string;
partnerApiServer?: string;
profileImageUrl?: string;
showContactGivesInline?: boolean;
passkeyExpirationMinutes?: number;
showGeneralAdvanced?: boolean;
showShortcutBvc?: boolean;
warnIfProdServer?: boolean;
warnIfTestServer?: boolean;
webPushServer?: string;
}
/**
* Interface for user profile data from API
*/
export interface UserProfileData {
description?: string;
locLat?: number;
locLon?: number;
}
/**
* Interface for API response containing user profile
*/
export interface UserProfileResponse {
data: UserProfileData;
}
/**
* Interface for component state related to profile management
*/
export interface ProfileState {
userProfileDesc: string;
userProfileLatitude: number;
userProfileLongitude: number;
includeUserProfileLocation: boolean;
savingProfile: boolean;
profileImageUrl?: string;
}
/**
* Interface for component state related to notifications
*/
export interface NotificationState {
notifyingNewActivity: boolean;
notifyingNewActivityTime: string;
notifyingReminder: boolean;
notifyingReminderMessage: string;
notifyingReminderTime: string;
subscription: PushSubscription | null;
}
/**
* Interface for component state related to settings
*/
export interface SettingsState {
activeDid: string;
apiServer: string;
apiServerInput: string;
partnerApiServer: string;
partnerApiServerInput: string;
webPushServer: string;
webPushServerInput: string;
passkeyExpirationMinutes: number;
previousPasskeyExpirationMinutes: number;
passkeyExpirationDescription: string;
hideRegisterPromptOnNewContact: boolean;
isRegistered: boolean;
isSearchAreasSet: boolean;
showContactGives: boolean;
showGeneralAdvanced: boolean;
showShortcutBvc: boolean;
warnIfProdServer: boolean;
warnIfTestServer: boolean;
}
/**
* Interface for component state related to UI display
*/
export interface UIState {
loadingProfile: boolean;
loadingLimits: boolean;
showAdvanced: boolean;
showB64Copy: boolean;
showDidCopy: boolean;
showDerCopy: boolean;
showPubCopy: boolean;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
downloadUrl: string;
zoom: number;
}
/**
* Interface for component state related to limits and validation
*/
export interface LimitsState {
endorserLimits: EndorserRateLimits | null;
imageLimits: ImageRateLimits | null;
limitsMessage: string;
publicHex: string;
publicBase64: string;
derivationPath: string;
}
/**
* Interface for component state related to identity
*/
export interface IdentityState {
givenName: string;
}
/**
* Complete interface for AccountViewView component state
*/
export interface AccountViewState
extends ProfileState,
NotificationState,
SettingsState,
UIState,
LimitsState,
IdentityState {}
/**
* Interface for clipboard copy operations
*/
export interface ClipboardOperation {
text: string;
callback: () => void;
}
/**
* Interface for notification permission callback
*/
export interface NotificationPermissionCallback {
success: boolean;
timeText: string;
message?: string;
}
/**
* Interface for import/export operations
*/
export interface ImportExportState {
inputImportFileNameRef?: Blob;
}
/**
* Type for API error responses
*/
export interface ApiErrorResponse {
response?: {
data?: {
error?: { message?: string } | string;
};
status?: number;
};
}
/**
* Type guard for API errors
*/
export function isApiError(error: unknown): error is ApiErrorResponse {
return typeof error === "object" && error !== null && "response" in error;
}
/**
* Type guard for standard errors
*/
export function isError(error: unknown): error is Error {
return error instanceof Error;
}
/**
* Interface for file import content structure
*/
export interface ImportContent {
data?: {
data?: Array<{
tableName: string;
rows: Array<unknown>;
}>;
};
}
/**
* Interface for map ready callback
*/
export interface MapReadyCallback {
(map: L.Map): void;
}
/**
* Interface for mouse event handlers
*/
export interface MouseEventHandler {
(event: LeafletMouseEvent): void;
}

249
src/utils/notificationUtils.ts

@ -27,12 +27,12 @@ export interface NotificationHelper {
* Standard notification timeouts
*/
export const NOTIFICATION_TIMEOUTS = {
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
} as const;
/**
@ -61,8 +61,10 @@ export const NOTIFICATION_MESSAGES = {
SENT_BRIEF: "Sent...",
CONFIRMATION_SUBMITTED: "Confirmation submitted.",
ALREADY_CONFIRMED: "You already confirmed this claim.",
CANNOT_CONFIRM_ISSUER: "You cannot confirm this because you issued this claim.",
CANNOT_CONFIRM_HIDDEN: "You cannot confirm this because some people are hidden.",
CANNOT_CONFIRM_ISSUER:
"You cannot confirm this because you issued this claim.",
CANNOT_CONFIRM_HIDDEN:
"You cannot confirm this because some people are hidden.",
NOT_REGISTERED: "Someone needs to register you before you can confirm.",
NOT_A_GIVE: "This is not a giving action to confirm.",
} as const;
@ -70,143 +72,195 @@ export const NOTIFICATION_MESSAGES = {
/**
* Creates a notification helper with utility methods
*/
export function createNotificationHelper(notifyFn: (notification: NotificationIface, timeout?: number) => void): NotificationHelper {
export function createNotificationHelper(
notifyFn: (notification: NotificationIface, timeout?: number) => void,
): NotificationHelper {
return {
notify: notifyFn,
// Success notifications
success: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "success",
title: NOTIFICATION_TITLES.SUCCESS,
text,
}, timeout);
notifyFn(
{
group: "alert",
type: "success",
title: NOTIFICATION_TITLES.SUCCESS,
text,
},
timeout,
);
},
// Error notifications
error: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => {
notifyFn({
group: "alert",
type: "danger",
title: NOTIFICATION_TITLES.ERROR,
text,
}, timeout);
notifyFn(
{
group: "alert",
type: "danger",
title: NOTIFICATION_TITLES.ERROR,
text,
},
timeout,
);
},
// Warning notifications
warning: (text: string, timeout = NOTIFICATION_TIMEOUTS.LONG) => {
notifyFn({
group: "alert",
type: "warning",
title: NOTIFICATION_TITLES.WARNING,
text,
}, timeout);
notifyFn(
{
group: "alert",
type: "warning",
title: NOTIFICATION_TITLES.WARNING,
text,
},
timeout,
);
},
// Info notifications
info: (text: string, timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.INFO,
text,
}, timeout);
notifyFn(
{
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.INFO,
text,
},
timeout,
);
},
// Toast notifications (brief)
toast: (title: string, text?: string, timeout = NOTIFICATION_TIMEOUTS.BRIEF) => {
notifyFn({
group: "alert",
type: "toast",
title,
text,
}, timeout);
toast: (
title: string,
text?: string,
timeout = NOTIFICATION_TIMEOUTS.BRIEF,
) => {
notifyFn(
{
group: "alert",
type: "toast",
title,
text,
},
timeout,
);
},
// Clipboard copy notifications
copied: (item: string, timeout = NOTIFICATION_TIMEOUTS.SHORT) => {
notifyFn({
group: "alert",
type: "toast",
title: NOTIFICATION_TITLES.COPIED,
text: NOTIFICATION_MESSAGES.CLIPBOARD_COPIED(item),
}, timeout);
notifyFn(
{
group: "alert",
type: "toast",
title: NOTIFICATION_TITLES.COPIED,
text: NOTIFICATION_MESSAGES.CLIPBOARD_COPIED(item),
},
timeout,
);
},
// Sent brief notification
sent: (timeout = NOTIFICATION_TIMEOUTS.BRIEF) => {
notifyFn({
group: "alert",
type: "toast",
title: NOTIFICATION_TITLES.SENT,
}, timeout);
notifyFn(
{
group: "alert",
type: "toast",
title: NOTIFICATION_TITLES.SENT,
},
timeout,
);
},
// Confirmation modal
confirm: (text: string, onYes: () => Promise<void>, timeout = NOTIFICATION_TIMEOUTS.MODAL) => {
notifyFn({
group: "modal",
type: "confirm",
title: NOTIFICATION_TITLES.CONFIRM,
text,
onYes,
}, timeout);
confirm: (
text: string,
onYes: () => Promise<void>,
timeout = NOTIFICATION_TIMEOUTS.MODAL,
) => {
notifyFn(
{
group: "modal",
type: "confirm",
title: NOTIFICATION_TITLES.CONFIRM,
text,
onYes,
},
timeout,
);
},
// Standard confirmation messages
confirmationSubmitted: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "success",
title: NOTIFICATION_TITLES.SUCCESS,
text: NOTIFICATION_MESSAGES.CONFIRMATION_SUBMITTED,
}, timeout);
notifyFn(
{
group: "alert",
type: "success",
title: NOTIFICATION_TITLES.SUCCESS,
text: NOTIFICATION_MESSAGES.CONFIRMATION_SUBMITTED,
},
timeout,
);
},
alreadyConfirmed: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.ALREADY_CONFIRMED,
text: NOTIFICATION_MESSAGES.ALREADY_CONFIRMED,
}, timeout);
notifyFn(
{
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.ALREADY_CONFIRMED,
text: NOTIFICATION_MESSAGES.ALREADY_CONFIRMED,
},
timeout,
);
},
cannotConfirmIssuer: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.CANNOT_CONFIRM,
text: NOTIFICATION_MESSAGES.CANNOT_CONFIRM_ISSUER,
}, timeout);
notifyFn(
{
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.CANNOT_CONFIRM,
text: NOTIFICATION_MESSAGES.CANNOT_CONFIRM_ISSUER,
},
timeout,
);
},
cannotConfirmHidden: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.CANNOT_CONFIRM,
text: NOTIFICATION_MESSAGES.CANNOT_CONFIRM_HIDDEN,
}, timeout);
notifyFn(
{
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.CANNOT_CONFIRM,
text: NOTIFICATION_MESSAGES.CANNOT_CONFIRM_HIDDEN,
},
timeout,
);
},
notRegistered: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.NOT_REGISTERED,
text: NOTIFICATION_MESSAGES.NOT_REGISTERED,
}, timeout);
notifyFn(
{
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.NOT_REGISTERED,
text: NOTIFICATION_MESSAGES.NOT_REGISTERED,
},
timeout,
);
},
notAGive: (timeout = NOTIFICATION_TIMEOUTS.STANDARD) => {
notifyFn({
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.INFO,
text: NOTIFICATION_MESSAGES.NOT_A_GIVE,
}, timeout);
notifyFn(
{
group: "alert",
type: "info",
title: NOTIFICATION_TITLES.INFO,
text: NOTIFICATION_MESSAGES.NOT_A_GIVE,
},
timeout,
);
},
};
}
@ -217,7 +271,8 @@ export function createNotificationHelper(notifyFn: (notification: NotificationIf
export const NotificationMixin = {
computed: {
$notifyHelper() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return createNotificationHelper((this as any).$notify);
},
},
};
};

272
src/utils/notify.ts

@ -1,20 +1,23 @@
import { NotificationIface } from '../constants/app';
import { NotificationIface } from "../constants/app";
/**
* Simple notification utility functions
* Provides the most concise API for common notification patterns
*/
export type NotifyFunction = (notification: NotificationIface, timeout?: number) => void;
export type NotifyFunction = (
notification: NotificationIface,
timeout?: number,
) => void;
// Standard timeouts
export const TIMEOUTS = {
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
BRIEF: 1000, // Very brief toasts ("Sent..." messages)
SHORT: 2000, // Short notifications (clipboard copies, quick confirmations)
STANDARD: 3000, // Standard notifications (success messages, general info)
LONG: 5000, // Longer notifications (errors, warnings, important info)
VERY_LONG: 7000, // Very long notifications (complex operations)
MODAL: -1, // Modal confirmations (no auto-dismiss)
} as const;
/**
@ -23,114 +26,191 @@ export const TIMEOUTS = {
export function createNotifyHelpers(notify: NotifyFunction) {
return {
// Success notifications
success: (text: string, timeout?: number) =>
notify({ group: "alert", type: "success", title: "Success", text }, timeout || TIMEOUTS.STANDARD),
success: (text: string, timeout?: number) =>
notify(
{ group: "alert", type: "success", title: "Success", text },
timeout || TIMEOUTS.STANDARD,
),
// Error notifications
error: (text: string, timeout?: number) =>
notify({ group: "alert", type: "danger", title: "Error", text }, timeout || TIMEOUTS.LONG),
error: (text: string, timeout?: number) =>
notify(
{ group: "alert", type: "danger", title: "Error", text },
timeout || TIMEOUTS.LONG,
),
// Warning notifications
warning: (text: string, timeout?: number) =>
notify({ group: "alert", type: "warning", title: "Warning", text }, timeout || TIMEOUTS.LONG),
warning: (text: string, timeout?: number) =>
notify(
{ group: "alert", type: "warning", title: "Warning", text },
timeout || TIMEOUTS.LONG,
),
// Info notifications
info: (text: string, timeout?: number) =>
notify({ group: "alert", type: "info", title: "Info", text }, timeout || TIMEOUTS.STANDARD),
info: (text: string, timeout?: number) =>
notify(
{ group: "alert", type: "info", title: "Info", text },
timeout || TIMEOUTS.STANDARD,
),
// Toast notifications (brief)
toast: (title: string, text?: string, timeout?: number) =>
notify({ group: "alert", type: "toast", title, text }, timeout || TIMEOUTS.BRIEF),
toast: (title: string, text?: string, timeout?: number) =>
notify(
{ group: "alert", type: "toast", title, text },
timeout || TIMEOUTS.BRIEF,
),
// Clipboard copy notifications
copied: (item: string, timeout?: number) =>
notify({
group: "alert",
type: "toast",
title: "Copied",
text: `${item} was copied to the clipboard.`
}, timeout || TIMEOUTS.SHORT),
copied: (item: string, timeout?: number) =>
notify(
{
group: "alert",
type: "toast",
title: "Copied",
text: `${item} was copied to the clipboard.`,
},
timeout || TIMEOUTS.SHORT,
),
// Sent brief notification
sent: (timeout?: number) =>
notify({ group: "alert", type: "toast", title: "Sent..." }, timeout || TIMEOUTS.BRIEF),
sent: (timeout?: number) =>
notify(
{ group: "alert", type: "toast", title: "Sent..." },
timeout || TIMEOUTS.BRIEF,
),
// Confirmation modal
confirm: (text: string, onYes: () => Promise<void>, timeout?: number) =>
notify({
group: "modal",
type: "confirm",
title: "Confirm",
text,
onYes
}, timeout || TIMEOUTS.MODAL),
confirm: (text: string, onYes: () => Promise<void>, timeout?: number) =>
notify(
{
group: "modal",
type: "confirm",
title: "Confirm",
text,
onYes,
},
timeout || TIMEOUTS.MODAL,
),
// Standard confirmation messages
confirmationSubmitted: (timeout?: number) =>
notify({
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted."
}, timeout || TIMEOUTS.STANDARD),
confirmationSubmitted: (timeout?: number) =>
notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
timeout || TIMEOUTS.STANDARD,
),
// Common error patterns
genericError: (timeout?: number) =>
notify({
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong."
}, timeout || TIMEOUTS.LONG),
genericError: (timeout?: number) =>
notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Something went wrong.",
},
timeout || TIMEOUTS.LONG,
),
// Common success patterns
genericSuccess: (timeout?: number) =>
notify({
group: "alert",
type: "success",
title: "Success",
text: "Operation completed successfully."
}, timeout || TIMEOUTS.STANDARD),
genericSuccess: (timeout?: number) =>
notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Operation completed successfully.",
},
timeout || TIMEOUTS.STANDARD,
),
// Common confirmation patterns
alreadyConfirmed: (timeout?: number) =>
notify({
group: "alert",
type: "info",
title: "Already Confirmed",
text: "You already confirmed this claim."
}, timeout || TIMEOUTS.STANDARD),
cannotConfirmIssuer: (timeout?: number) =>
notify({
group: "alert",
type: "info",
title: "Cannot Confirm",
text: "You cannot confirm this because you issued this claim."
}, timeout || TIMEOUTS.STANDARD),
cannotConfirmHidden: (timeout?: number) =>
notify({
group: "alert",
type: "info",
title: "Cannot Confirm",
text: "You cannot confirm this because some people are hidden."
}, timeout || TIMEOUTS.STANDARD),
notRegistered: (timeout?: number) =>
notify({
group: "alert",
type: "info",
title: "Not Registered",
text: "Someone needs to register you before you can confirm."
}, timeout || TIMEOUTS.STANDARD),
notAGive: (timeout?: number) =>
notify({
group: "alert",
type: "info",
title: "Info",
text: "This is not a giving action to confirm."
}, timeout || TIMEOUTS.STANDARD),
alreadyConfirmed: (timeout?: number) =>
notify(
{
group: "alert",
type: "info",
title: "Already Confirmed",
text: "You already confirmed this claim.",
},
timeout || TIMEOUTS.STANDARD,
),
cannotConfirmIssuer: (timeout?: number) =>
notify(
{
group: "alert",
type: "info",
title: "Cannot Confirm",
text: "You cannot confirm this because you issued this claim.",
},
timeout || TIMEOUTS.STANDARD,
),
cannotConfirmHidden: (timeout?: number) =>
notify(
{
group: "alert",
type: "info",
title: "Cannot Confirm",
text: "You cannot confirm this because some people are hidden.",
},
timeout || TIMEOUTS.STANDARD,
),
notRegistered: (timeout?: number) =>
notify(
{
group: "alert",
type: "info",
title: "Not Registered",
text: "Someone needs to register you before you can confirm.",
},
timeout || TIMEOUTS.STANDARD,
),
notAGive: (timeout?: number) =>
notify(
{
group: "alert",
type: "info",
title: "Info",
text: "This is not a giving action to confirm.",
},
timeout || TIMEOUTS.STANDARD,
),
// Notification-off modal (for turning off notifications)
notificationOff: (
title: string,
callback: (success: boolean) => Promise<void>,
timeout?: number,
) =>
notify(
{
group: "modal",
type: "notification-off",
title,
text: "", // unused, only here to satisfy type check
callback,
},
timeout || TIMEOUTS.MODAL,
),
// Download notifications
downloadStarted: (format: string = "Dexie", timeout?: number) =>
notify(
{
group: "alert",
type: "success",
title: "Download Started",
text: `See your downloads directory for the backup. It is in the ${format} format.`,
},
timeout || TIMEOUTS.MODAL,
),
};
}
}

594
src/views/AccountViewView.vue

@ -1030,24 +1030,18 @@ import {
import { UserProfile } from "@/libs/partnerServer";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import {
AccountSettings,
UserProfileResponse,
isApiError,
isError,
ImportContent,
} from "@/interfaces/accountView";
const inputImportFileNameRef = ref<Blob>();
// Type guard for API errors
function isApiError(error: unknown): error is {
response?: {
data?: { error?: { message?: string } | string };
status?: number;
};
} {
return typeof error === "object" && error !== null && "response" in error;
}
// Type guard for standard errors
function isError(error: unknown): error is Error {
return error instanceof Error;
}
// Helper function to extract error message
function extractErrorMessage(error: unknown): string {
if (isApiError(error)) {
@ -1085,59 +1079,72 @@ export default class AccountViewView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER;
DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER;
activeDid = "";
apiServer = "";
apiServerInput = "";
derivationPath = "";
downloadUrl = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
endorserLimits: EndorserRateLimits | null = null;
givenName = "";
hideRegisterPromptOnNewContact = false;
imageLimits: ImageRateLimits | null = null;
includeUserProfileLocation = false;
isRegistered = false;
isSearchAreasSet = false;
limitsMessage = "";
loadingLimits = false;
loadingProfile = true;
notifyingNewActivity = false;
notifyingNewActivityTime = "";
notifyingReminder = false;
notifyingReminderMessage = "";
notifyingReminderTime = "";
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
partnerApiServerInput = DEFAULT_PARTNER_API_SERVER;
passkeyExpirationDescription = "";
passkeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
// Add notification helpers
private notify = createNotifyHelpers(this.$notify);
// Constants
readonly AppConstants: typeof AppString = AppString;
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;
// Identity and settings properties
activeDid: string = "";
apiServer: string = "";
apiServerInput: string = "";
derivationPath: string = "";
givenName: string = "";
hideRegisterPromptOnNewContact: boolean = false;
isRegistered: boolean = false;
isSearchAreasSet: boolean = false;
partnerApiServer: string = DEFAULT_PARTNER_API_SERVER;
partnerApiServerInput: string = DEFAULT_PARTNER_API_SERVER;
passkeyExpirationDescription: string = "";
passkeyExpirationMinutes: number = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes: number = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
profileImageUrl?: string;
publicHex = "";
publicBase64 = "";
savingProfile = false;
showAdvanced = false;
showB64Copy = false;
showContactGives = false;
showDidCopy = false;
showDerCopy = false;
showGeneralAdvanced = false;
publicHex: string = "";
publicBase64: string = "";
webPushServer: string = DEFAULT_PUSH_SERVER;
webPushServerInput: string = DEFAULT_PUSH_SERVER;
// Profile properties
userProfileDesc: string = "";
userProfileLatitude: number = 0;
userProfileLongitude: number = 0;
includeUserProfileLocation: boolean = false;
savingProfile: boolean = false;
// Notification properties
notifyingNewActivity: boolean = false;
notifyingNewActivityTime: string = "";
notifyingReminder: boolean = false;
notifyingReminderMessage: string = "";
notifyingReminderTime: string = "";
subscription: PushSubscription | null = null;
// UI state properties
downloadUrl: string = ""; // because DuckDuckGo doesn't download on automated call to "click" on the anchor
loadingLimits: boolean = false;
loadingProfile: boolean = true;
showAdvanced: boolean = false;
showB64Copy: boolean = false;
showContactGives: boolean = false;
showDidCopy: boolean = false;
showDerCopy: boolean = false;
showGeneralAdvanced: boolean = false;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
showPubCopy = false;
showShortcutBvc = false;
subscription: PushSubscription | null = null;
warnIfProdServer = false;
warnIfTestServer = false;
webPushServer = DEFAULT_PUSH_SERVER;
webPushServerInput = DEFAULT_PUSH_SERVER;
userProfileDesc = "";
userProfileLatitude = 0;
userProfileLongitude = 0;
zoom = 2;
showPubCopy: boolean = false;
showShortcutBvc: boolean = false;
warnIfProdServer: boolean = false;
warnIfTestServer: boolean = false;
zoom: number = 2;
// Limits and validation properties
endorserLimits: EndorserRateLimits | null = null;
imageLimits: ImageRateLimits | null = null;
limitsMessage: string = "";
/**
* Async function executed when the component is mounted.
@ -1146,7 +1153,7 @@ export default class AccountViewView extends Vue {
*
* @throws Will display specific messages to the user based on different errors.
*/
async mounted() {
async mounted(): Promise<void> {
try {
// Initialize component state with values from the database or defaults
await this.initializeState();
@ -1156,7 +1163,7 @@ export default class AccountViewView extends Vue {
if (this.isRegistered) {
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get(
const response = await this.axios.get<UserProfileResponse>(
this.partnerApiServer +
"/api/partner/userProfileForIssuer/" +
this.activeDid,
@ -1171,7 +1178,7 @@ export default class AccountViewView extends Vue {
}
} else {
// won't get here because axios throws an error instead
throw Error("Unable to load profile.");
throw Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE);
}
} catch (error) {
if (isApiError(error) && error.response?.status === 404) {
@ -1180,14 +1187,8 @@ export default class AccountViewView extends Vue {
databaseUtil.logConsoleAndDb(
"Error loading profile: " + errorStringForLog(error),
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Profile",
text: "Your server profile is not available.",
},
5000,
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
);
}
}
@ -1203,15 +1204,7 @@ export default class AccountViewView extends Vue {
"To repeat with concatenated error: telling user to clear cache at page create because: " +
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Profile",
text: "See the Help page about errors with your personal data.",
},
5000,
);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_LOAD_ERROR);
} finally {
this.loadingProfile = false;
}
@ -1231,14 +1224,9 @@ export default class AccountViewView extends Vue {
}
}
} catch (error) {
this.$notify(
{
group: "alert",
type: "warning",
title: "Cannot Set Notifications",
text: "This browser does not support notifications. Use Chrome, or install this to the home screen, or try other suggestions on the 'Troubleshoot your notifications' page.",
},
7000,
this.notify.warning(
ACCOUNT_VIEW_CONSTANTS.ERRORS.BROWSER_NOTIFICATIONS_UNSUPPORTED,
TIMEOUTS.VERY_LONG,
);
}
} else {
@ -1249,7 +1237,7 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
beforeUnmount() {
beforeUnmount(): void {
if (this.downloadUrl) {
URL.revokeObjectURL(this.downloadUrl);
}
@ -1258,8 +1246,9 @@ export default class AccountViewView extends Vue {
/**
* Initializes component state with values from the database or defaults.
*/
async initializeState() {
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
async initializeState(): Promise<void> {
const settings: AccountSettings =
await databaseUtil.retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@ -1293,56 +1282,56 @@ export default class AccountViewView extends Vue {
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void) {
doCopyTwoSecRedo(text: string, fn: () => void): void {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
}
async toggleShowContactAmounts() {
async toggleShowContactAmounts(): Promise<void> {
this.showContactGives = !this.showContactGives;
await this.$saveSettings({
showContactGivesInline: this.showContactGives,
});
}
async toggleShowGeneralAdvanced() {
async toggleShowGeneralAdvanced(): Promise<void> {
this.showGeneralAdvanced = !this.showGeneralAdvanced;
await this.$saveSettings({
showGeneralAdvanced: this.showGeneralAdvanced,
});
}
async toggleProdWarning() {
async toggleProdWarning(): Promise<void> {
this.warnIfProdServer = !this.warnIfProdServer;
await this.$saveSettings({
warnIfProdServer: this.warnIfProdServer,
});
}
async toggleTestWarning() {
async toggleTestWarning(): Promise<void> {
this.warnIfTestServer = !this.warnIfTestServer;
await this.$saveSettings({
warnIfTestServer: this.warnIfTestServer,
});
}
async toggleShowShortcutBvc() {
async toggleShowShortcutBvc(): Promise<void> {
this.showShortcutBvc = !this.showShortcutBvc;
await this.$saveSettings({
showShortcutBvc: this.showShortcutBvc,
});
}
readableDate(timeStr: string) {
readableDate(timeStr: string): string {
return timeStr ? timeStr.substring(0, timeStr.indexOf("T")) : "?";
}
/**
* Processes the identity and updates the component's state.
*/
async processIdentity() {
async processIdentity(): Promise<void> {
const account = await retrieveAccountMetadata(this.activeDid);
if (account?.identity) {
const identity = JSON.parse(account.identity as string) as IIdentifier;
@ -1359,30 +1348,18 @@ export default class AccountViewView extends Vue {
}
}
async showNewActivityNotificationInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "New Activity Notification",
text: `
This will only notify you when there is new relevant activity for you personally.
Note that it runs on your device and many factors may affect delivery,
so if you want a reliable but simple daily notification then choose a 'Reminder'.
Do you want more details?
`,
onYes: async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
yesText: "tell me more.",
async showNewActivityNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.NEW_ACTIVITY_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
-1,
);
}
async showNewActivityNotificationChoice() {
async showNewActivityNotificationChoice(): Promise<void> {
if (!this.notifyingNewActivity) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
@ -1396,51 +1373,30 @@ export default class AccountViewView extends Vue {
}
});
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: DAILY_CHECK_TITLE, // repurposed to indicate the type of notification
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
},
},
-1,
);
this.notify.notificationOff(DAILY_CHECK_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingNewActivityTime: "",
});
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
});
}
}
async showReminderNotificationInfo() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Reminder Notification",
text: `
This will notify you at a specific time each day.
Note that it does not give you personalized notifications,
so if you want less reliable but personalized notification then choose a 'New Activity' Notification.
Do you want more details?
`,
onYes: async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
yesText: "tell me more.",
async showReminderNotificationInfo(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.NOTIFICATIONS.REMINDER_INFO,
async () => {
await (this.$router as Router).push({
name: "help-notification-types",
});
},
-1,
);
}
async showReminderNotificationChoice() {
async showReminderNotificationChoice(): Promise<void> {
if (!this.notifyingReminder) {
(
this.$refs.pushNotificationPermission as PushNotificationPermission
@ -1459,30 +1415,21 @@ export default class AccountViewView extends Vue {
},
);
} else {
this.$notify(
{
group: "modal",
type: "notification-off",
title: DIRECT_PUSH_TITLE, // repurposed to indicate the type of notification
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
},
},
-1,
);
this.notify.notificationOff(DIRECT_PUSH_TITLE, async (success) => {
if (success) {
await this.$saveSettings({
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
}
});
}
}
public async toggleHideRegisterPromptOnNewContact() {
public async toggleHideRegisterPromptOnNewContact(): Promise<void> {
const newSetting = !this.hideRegisterPromptOnNewContact;
await this.$saveSettings({
hideRegisterPromptOnNewContact: newSetting,
@ -1490,7 +1437,7 @@ export default class AccountViewView extends Vue {
this.hideRegisterPromptOnNewContact = newSetting;
}
public async updatePasskeyExpiration() {
public async updatePasskeyExpiration(): Promise<void> {
await this.$saveSettings({
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
});
@ -1498,7 +1445,7 @@ export default class AccountViewView extends Vue {
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
public async turnOffNotifyingFlags() {
public async turnOffNotifyingFlags(): Promise<void> {
// should tell the push server as well
await this.$saveSettings({
notifyingNewActivityTime: "",
@ -1586,15 +1533,7 @@ export default class AccountViewView extends Vue {
* Notifies the user that the download has started.
*/
private notifyDownloadStarted() {
this.$notify(
{
group: "alert",
type: "success",
title: "Download Started",
text: "See your downloads directory for the backup. It is in the Dexie format.",
},
-1,
);
this.notify.downloadStarted();
}
/**
@ -1602,42 +1541,29 @@ export default class AccountViewView extends Vue {
*
* @param {Error} error - The error object.
*/
private handleExportError(error: unknown) {
private handleExportError(error: unknown): void {
logger.error("Export Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Export Error",
text: "There was an error exporting the data.",
},
3000,
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.EXPORT_ERROR,
TIMEOUTS.STANDARD,
);
}
async uploadImportFile(event: Event) {
async uploadImportFile(event: Event): Promise<void> {
inputImportFileNameRef.value = (
event.target as HTMLInputElement
).files?.[0];
}
showContactImport() {
showContactImport(): boolean {
return !!inputImportFileNameRef.value;
}
confirmSubmitImportFile() {
confirmSubmitImportFile(): void {
if (inputImportFileNameRef.value != null) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Replace All",
text:
"This will replace all settings and contacts, so we recommend you first do the backup step above." +
" Are you sure you want to import and replace all contacts and settings?",
onYes: this.submitImportFile,
},
-1,
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.WARNINGS.IMPORT_REPLACE_WARNING,
this.submitImportFile,
);
}
}
@ -1647,18 +1573,18 @@ export default class AccountViewView extends Vue {
*
* @throws Will notify the user if there is an export error.
*/
async submitImportFile() {
async submitImportFile(): Promise<void> {
if (inputImportFileNameRef.value != null) {
// TODO: implement this for SQLite
}
}
async checkContactImports() {
async checkContactImports(): Promise<void> {
const reader = new FileReader();
reader.onload = (event) => {
const fileContent: string = (event.target?.result as string) || "{}";
try {
const contents = JSON.parse(fileContent);
const contents: ImportContent = JSON.parse(fileContent);
const contactTableRows: Array<Contact> = (
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
)?.find((table) => table.tableName === "contacts")
@ -1673,45 +1599,34 @@ export default class AccountViewView extends Vue {
});
} catch (error) {
logger.error("Error checking contact imports:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Importing",
text: "There was an error reading that Dexie file.",
},
3000,
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMPORT_ERROR,
TIMEOUTS.STANDARD,
);
}
};
reader.readAsText(inputImportFileNameRef.value as Blob);
}
private progressCallback(progress: ImportProgress) {
private progressCallback(progress: ImportProgress): boolean {
logger.log(
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
);
if (progress.done) {
// console.log(`Imported ${progress.completedTables} tables.`);
this.$notify(
{
group: "alert",
type: "success",
title: "Import Complete",
text: "",
},
5000,
this.notify.success(
ACCOUNT_VIEW_CONSTANTS.SUCCESS.IMPORT_COMPLETE,
TIMEOUTS.LONG,
);
}
return true;
}
async checkLimits() {
async checkLimits(): Promise<void> {
if (this.activeDid) {
this.checkLimitsFor(this.activeDid);
} else {
this.limitsMessage =
"You have no identifier, or your data has been corrupted.";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
}
}
@ -1722,7 +1637,7 @@ export default class AccountViewView extends Vue {
*
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
*/
private async checkLimitsFor(did: string) {
private async checkLimitsFor(did: string): Promise<void> {
this.loadingLimits = true;
this.limitsMessage = "";
@ -1743,14 +1658,8 @@ export default class AccountViewView extends Vue {
this.isRegistered = true;
} catch (err) {
logger.error("Got an error updating settings:", err);
this.$notify(
{
group: "alert",
type: "danger",
title: "Update Error",
text: "Unable to update your settings. Check claim limits again.",
},
5000,
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.SETTINGS_UPDATE_ERROR,
);
}
}
@ -1759,10 +1668,11 @@ export default class AccountViewView extends Vue {
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
this.limitsMessage = "You don't have access to upload images.";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
}
} catch {
this.limitsMessage = "You cannot upload images.";
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES;
}
}
} catch (error) {
@ -1777,7 +1687,7 @@ export default class AccountViewView extends Vue {
*
* @param {AxiosError | Error} error - The error object.
*/
private handleRateLimitsError(error: unknown) {
private handleRateLimitsError(error: unknown): void {
if (error instanceof AxiosError) {
if (error.status == 400 || error.status == 404) {
// no worries: they probably just aren't registered and don't have any limits
@ -1785,50 +1695,44 @@ export default class AccountViewView extends Vue {
"Got 400 or 404 response retrieving limits which probably means they're not registered:",
error,
);
this.limitsMessage = "No limits were found, so no actions are allowed.";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
} else {
const data = error.response?.data as ErrorResponse;
this.limitsMessage =
(data?.error?.message as string) || "Bad server response.";
(data?.error?.message as string) ||
ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE;
logger.error("Got bad response retrieving limits:", error);
}
} else {
this.limitsMessage = "Got an error retrieving limits.";
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
logger.error("Got some error retrieving limits:", error);
}
}
async onClickSaveApiServer() {
async onClickSaveApiServer(): Promise<void> {
await databaseUtil.updateDefaultSettings({
apiServer: this.apiServerInput,
});
this.apiServer = this.apiServerInput;
}
async onClickSavePartnerServer() {
async onClickSavePartnerServer(): Promise<void> {
await databaseUtil.updateDefaultSettings({
partnerApiServer: this.partnerApiServerInput,
});
this.partnerApiServer = this.partnerApiServerInput;
}
async onClickSavePushServer() {
async onClickSavePushServer(): Promise<void> {
await databaseUtil.updateDefaultSettings({
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
this.$notify(
{
group: "alert",
type: "warning",
title: "Reload",
text: "Now reload the app to get a new VAPID to use with this push server.",
},
5000,
);
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.INFO.RELOAD_VAPID);
}
openImageDialog() {
openImageDialog(): void {
(this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => {
await databaseUtil.updateDefaultSettings({
@ -1842,21 +1746,14 @@ export default class AccountViewView extends Vue {
);
}
confirmDeleteImage() {
this.$notify(
{
group: "modal",
type: "confirm",
title:
"Note that anyone with you already as a contact will no longer see a picture, and you will have to reshare your data with them if you save a new picture. Are you sure you want to delete your profile picture?",
text: "",
onYes: this.deleteImage,
},
-1,
confirmDeleteImage(): void {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.WARNINGS.IMAGE_DELETE_WARNING,
this.deleteImage,
);
}
async deleteImage() {
async deleteImage(): Promise<void> {
if (!this.profileImageUrl) {
return;
}
@ -1882,15 +1779,7 @@ export default class AccountViewView extends Vue {
// (either they'll simply continue or they're canceling and going back)
} else {
logger.error("Non-success deleting image:", response);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem deleting the image. Contact support if you want it removed from the servers.",
},
5000,
);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.IMAGE_DELETE_PROBLEM);
// keep the imageUrl in localStorage so the user can try again if they want
}
@ -1913,38 +1802,28 @@ export default class AccountViewView extends Vue {
// it already doesn't exist so we won't say anything to the user
} else {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was an error deleting the image.",
},
3000,
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMAGE_DELETE_ERROR,
TIMEOUTS.STANDARD,
);
}
}
}
onMapReady(map: L.Map) {
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);
}
showProfileInfo() {
this.$notify(
{
group: "alert",
type: "info",
title: "Public Profile Information",
text: "This data will be published for all to see, so be careful what your write. Your ID will only be shared with people who you allow to see your activity.",
},
7000,
showProfileInfo(): void {
this.notify.info(
ACCOUNT_VIEW_CONSTANTS.INFO.PROFILE_INFO,
TIMEOUTS.VERY_LONG,
);
}
async saveProfile() {
async saveProfile(): Promise<void> {
this.savingProfile = true;
try {
const headers = await getHeaders(this.activeDid);
@ -1955,14 +1834,10 @@ export default class AccountViewView extends Vue {
payload.locLat = this.userProfileLatitude;
payload.locLon = this.userProfileLongitude;
} else if (this.includeUserProfileLocation) {
this.$notify(
{
group: "alert",
type: "toast",
title: "",
text: "No profile location is saved.",
},
3000,
this.notify.toast(
"",
ACCOUNT_VIEW_CONSTANTS.INFO.NO_PROFILE_LOCATION,
TIMEOUTS.STANDARD,
);
}
const response = await this.axios.post(
@ -1971,40 +1846,28 @@ export default class AccountViewView extends Vue {
{ headers },
);
if (response.status === 201) {
this.$notify(
{
group: "alert",
type: "success",
title: "Profile Saved",
text: "Your profile has been updated successfully.",
},
3000,
this.notify.success(
ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED,
TIMEOUTS.STANDARD,
);
} else {
// won't get here because axios throws an error on non-success
throw Error("Profile not saved");
throw Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
}
} catch (error) {
databaseUtil.logConsoleAndDb(
"Error saving profile: " + errorStringForLog(error),
);
const errorMessage: string =
extractErrorMessage(error) || "There was an error saving your profile.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Saving Profile",
text: errorMessage,
},
3000,
);
extractErrorMessage(error) ||
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR;
this.notify.error(errorMessage, TIMEOUTS.STANDARD);
} finally {
this.savingProfile = false;
}
}
toggleUserProfileLocation() {
toggleUserProfileLocation(): void {
this.includeUserProfileLocation = !this.includeUserProfileLocation;
if (!this.includeUserProfileLocation) {
this.userProfileLatitude = 0;
@ -2013,42 +1876,30 @@ export default class AccountViewView extends Vue {
}
}
confirmEraseLatLong() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Erase Marker",
text: "Are you sure you don't want to mark a location? This will erase the current location.",
onYes: async () => {
this.eraseLatLong();
},
confirmEraseLatLong(): void {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.WARNINGS.ERASE_LOCATION_WARNING,
async () => {
this.eraseLatLong();
},
-1,
);
}
eraseLatLong() {
eraseLatLong(): void {
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
this.zoom = 2;
this.includeUserProfileLocation = false;
}
async confirmDeleteProfile() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Delete Profile",
text: "Are you sure you want to delete your public profile? This will remove your description and location from the server, and it cannot be undone.",
onYes: this.deleteProfile,
},
-1,
async confirmDeleteProfile(): Promise<void> {
this.notify.confirm(
ACCOUNT_VIEW_CONSTANTS.WARNINGS.DELETE_PROFILE_WARNING,
this.deleteProfile,
);
}
async deleteProfile() {
async deleteProfile(): Promise<void> {
this.savingProfile = true;
try {
const headers = await getHeaders(this.activeDid);
@ -2061,17 +1912,12 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
this.includeUserProfileLocation = false;
this.$notify(
{
group: "alert",
type: "success",
title: "Profile Deleted",
text: "Your profile has been deleted successfully.",
},
3000,
this.notify.success(
ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED,
TIMEOUTS.STANDARD,
);
} else {
throw Error("Profile not deleted");
throw Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_DELETED);
}
} catch (error) {
databaseUtil.logConsoleAndDb(
@ -2079,16 +1925,8 @@ export default class AccountViewView extends Vue {
);
const errorMessage: string =
extractErrorMessage(error) ||
"There was an error deleting your profile.";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Deleting Profile",
text: errorMessage,
},
3000,
);
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR;
this.notify.error(errorMessage, TIMEOUTS.STANDARD);
} finally {
this.savingProfile = false;
}

21
src/views/ClaimView.vue

@ -732,7 +732,9 @@ export default class ClaimView extends Vue {
"Error retrieving all account DIDs on home page:" + error,
true,
);
this.notify.error("See the Help page for problems with your personal data.");
this.notify.error(
"See the Help page for problems with your personal data.",
);
}
const claimId = this.$route.params.id as string;
@ -875,7 +877,10 @@ export default class ClaimView extends Vue {
await this.$logError(
"Error retrieving claim: " + JSON.stringify(serverError),
);
this.notify.error("Something went wrong retrieving claim data.", TIMEOUTS.STANDARD);
this.notify.error(
"Something went wrong retrieving claim data.",
TIMEOUTS.STANDARD,
);
}
}
@ -932,7 +937,10 @@ export default class ClaimView extends Vue {
" if they can find out more and make an introduction: " +
" send them this page and see if they can make a connection for you.";
} else {
this.notify.error("Something went wrong retrieving that claim.", TIMEOUTS.LONG);
this.notify.error(
"Something went wrong retrieving that claim.",
TIMEOUTS.LONG,
);
}
}
}
@ -942,7 +950,7 @@ export default class ClaimView extends Vue {
"Do you personally confirm that this is true?",
async () => {
await this.confirmClaim();
}
},
);
}
@ -1051,7 +1059,10 @@ export default class ClaimView extends Vue {
await this.$logError(
"Unrecognized claim type for edit: " + this.veriClaim.claimType,
);
this.notify.error("This is an unrecognized claim type.", TIMEOUTS.STANDARD);
this.notify.error(
"This is an unrecognized claim type.",
TIMEOUTS.STANDARD,
);
}
}
}

Loading…
Cancel
Save