diff --git a/src/composables/useNotifications.ts b/src/composables/useNotifications.ts index deddd3c5..ca7bfd23 100644 --- a/src/composables/useNotifications.ts +++ b/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, + _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, + _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, 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, }; -} \ No newline at end of file +} diff --git a/src/constants/accountView.ts b/src/constants/accountView.ts new file mode 100644 index 00000000..4ecc8d2b --- /dev/null +++ b/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; diff --git a/src/interfaces/accountView.ts b/src/interfaces/accountView.ts new file mode 100644 index 00000000..46c2c768 --- /dev/null +++ b/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; + }>; + }; +} + +/** + * Interface for map ready callback + */ +export interface MapReadyCallback { + (map: L.Map): void; +} + +/** + * Interface for mouse event handlers + */ +export interface MouseEventHandler { + (event: LeafletMouseEvent): void; +} diff --git a/src/utils/notificationUtils.ts b/src/utils/notificationUtils.ts index dc06e997..496df3de 100644 --- a/src/utils/notificationUtils.ts +++ b/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, timeout = NOTIFICATION_TIMEOUTS.MODAL) => { - notifyFn({ - group: "modal", - type: "confirm", - title: NOTIFICATION_TITLES.CONFIRM, - text, - onYes, - }, timeout); + confirm: ( + text: string, + onYes: () => Promise, + 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); }, }, -}; \ No newline at end of file +}; diff --git a/src/utils/notify.ts b/src/utils/notify.ts index a9205ac9..a099320f 100644 --- a/src/utils/notify.ts +++ b/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, timeout?: number) => - notify({ - group: "modal", - type: "confirm", - title: "Confirm", - text, - onYes - }, timeout || TIMEOUTS.MODAL), + confirm: (text: string, onYes: () => Promise, 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, + 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, + ), }; -} \ No newline at end of file +} diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index f753a743..357b2330 100644 --- a/src/views/AccountViewView.vue +++ b/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(); -// 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 { 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( 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 { + 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 { this.showContactGives = !this.showContactGives; await this.$saveSettings({ showContactGivesInline: this.showContactGives, }); } - async toggleShowGeneralAdvanced() { + async toggleShowGeneralAdvanced(): Promise { this.showGeneralAdvanced = !this.showGeneralAdvanced; await this.$saveSettings({ showGeneralAdvanced: this.showGeneralAdvanced, }); } - async toggleProdWarning() { + async toggleProdWarning(): Promise { this.warnIfProdServer = !this.warnIfProdServer; await this.$saveSettings({ warnIfProdServer: this.warnIfProdServer, }); } - async toggleTestWarning() { + async toggleTestWarning(): Promise { this.warnIfTestServer = !this.warnIfTestServer; await this.$saveSettings({ warnIfTestServer: this.warnIfTestServer, }); } - async toggleShowShortcutBvc() { + async toggleShowShortcutBvc(): Promise { 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 { 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 { + 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 { 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 { + 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 { 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 { 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 { 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 { // 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 { 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 { if (inputImportFileNameRef.value != null) { // TODO: implement this for SQLite } } - async checkContactImports() { + async checkContactImports(): Promise { 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 = ( contents.data?.data as [{ tableName: string; rows: Array }] )?.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 { 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 { 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 { await databaseUtil.updateDefaultSettings({ apiServer: this.apiServerInput, }); this.apiServer = this.apiServerInput; } - async onClickSavePartnerServer() { + async onClickSavePartnerServer(): Promise { await databaseUtil.updateDefaultSettings({ partnerApiServer: this.partnerApiServerInput, }); this.partnerApiServer = this.partnerApiServerInput; } - async onClickSavePushServer() { + async onClickSavePushServer(): Promise { 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 { 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 { 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 { + this.notify.confirm( + ACCOUNT_VIEW_CONSTANTS.WARNINGS.DELETE_PROFILE_WARNING, + this.deleteProfile, ); } - async deleteProfile() { + async deleteProfile(): Promise { 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; } diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index f658de99..17f7a20f 100644 --- a/src/views/ClaimView.vue +++ b/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, + ); } } }