Refactor AccountViewView.vue to use notify helper and PlatformServiceMixin

- Migrated all notification calls in AccountViewView.vue to use a notify helper initialized in mounted(), ensuring this.$notify is available and preventing runtime errors.
- Removed NotificationMixin and $notifyHelper usage; restored and standardized notify helper pattern.
- Migrated all database and platform service operations to use PlatformServiceMixin ultra-concise methods ($accountSettings, $saveSettings, $saveUserSettings, etc.).
- Cleaned up unused imports and code related to previous notification and database patterns.
- Ensured all linter errors and warnings are resolved in both AccountViewView.vue and notification utility files.
This commit is contained in:
Matthew Raymer
2025-07-05 12:42:31 +00:00
parent f901b6b751
commit d3042ec955
2 changed files with 325 additions and 238 deletions

View File

@@ -975,7 +975,6 @@
<script lang="ts">
import "leaflet/dist/leaflet.css";
import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there
@@ -1007,16 +1006,10 @@ import {
} from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import {
EndorserRateLimits,
ImageRateLimits,
ErrorResponse,
} from "../interfaces";
import { EndorserRateLimits, ImageRateLimits } from "../interfaces";
import {
clearPasskeyToken,
errorStringForLog,
fetchEndorserRateLimits,
fetchImageRateLimits,
getHeaders,
@@ -1027,38 +1020,23 @@ import {
DIRECT_PUSH_TITLE,
retrieveAccountMetadata,
} from "../libs/util";
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";
import {
ProfileService,
createProfileService,
ProfileData,
} from "@/services/ProfileService";
const inputImportFileNameRef = ref<Blob>();
// Helper function to extract error message
function extractErrorMessage(error: unknown): string {
if (isApiError(error)) {
const apiError = error.response?.data?.error;
if (typeof apiError === "string") {
return apiError;
}
if (typeof apiError === "object" && apiError?.message) {
return apiError.message;
}
}
if (isError(error)) {
return error.message;
}
return "An unknown error occurred";
}
@Component({
components: {
EntityIcon,
@@ -1079,9 +1057,6 @@ export default class AccountViewView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
// Add notification helpers
private notify = createNotifyHelpers(this.$notify);
// Constants
readonly AppConstants: typeof AppString = AppString;
readonly DEFAULT_PUSH_SERVER: string = DEFAULT_PUSH_SERVER;
@@ -1146,6 +1121,9 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
limitsMessage: string = "";
private profileService!: ProfileService;
private notify!: ReturnType<typeof createNotifyHelpers>;
/**
* Async function executed when the component is mounted.
* Initializes the component's state with values from the database,
@@ -1154,55 +1132,34 @@ export default class AccountViewView extends Vue {
* @throws Will display specific messages to the user based on different errors.
*/
async mounted(): Promise<void> {
this.notify = createNotifyHelpers(this.$notify);
this.profileService = createProfileService(this.axios, this.partnerApiServer);
try {
// Initialize component state with values from the database or defaults
await this.initializeState();
await this.processIdentity();
// Load the user profile
if (this.isRegistered) {
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.get<UserProfileResponse>(
this.partnerApiServer +
"/api/partner/userProfileForIssuer/" +
this.activeDid,
{ headers },
);
if (response.status === 200) {
this.userProfileDesc = response.data.data.description || "";
this.userProfileLatitude = response.data.data.locLat || 0;
this.userProfileLongitude = response.data.data.locLon || 0;
if (this.userProfileLatitude && this.userProfileLongitude) {
this.includeUserProfileLocation = true;
}
const profile = await this.profileService.loadProfile(this.activeDid);
if (profile) {
this.userProfileDesc = profile.description;
this.userProfileLatitude = profile.latitude;
this.userProfileLongitude = profile.longitude;
this.includeUserProfileLocation = profile.includeLocation;
} else {
// won't get here because axios throws an error instead
throw Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE);
// Profile not created yet; leave defaults
}
} catch (error) {
if (isApiError(error) && error.response?.status === 404) {
// this is ok: the profile is not yet created
} else {
databaseUtil.logConsoleAndDb(
"Error loading profile: " + errorStringForLog(error),
);
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
);
}
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE);
}
}
} catch (error) {
// this can happen when running automated tests in dev mode because notifications don't work
logger.error(
"Telling user to clear cache at page create because:",
error,
);
// this sometimes gives different information on the error
logger.error(
"To repeat with concatenated error: telling user to clear cache at page create because: " +
error,
"To repeat with concatenated error: telling user to clear cache at page create because: " + error,
);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_LOAD_ERROR);
} finally {
@@ -1247,8 +1204,7 @@ export default class AccountViewView extends Vue {
* Initializes component state with values from the database or defaults.
*/
async initializeState(): Promise<void> {
const settings: AccountSettings =
await databaseUtil.retrieveSettingsForActiveAccount();
const settings: AccountSettings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@@ -1623,109 +1579,77 @@ export default class AccountViewView extends Vue {
}
async checkLimits(): Promise<void> {
if (this.activeDid) {
this.checkLimitsFor(this.activeDid);
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
}
}
/**
* Use "checkLimits" instead.
*
* Asynchronously checks rate limits for the given identity.
*
* Updates component state variables `limits`, `limitsMessage`, and `loadingLimits`.
*/
private async checkLimitsFor(did: string): Promise<void> {
this.loadingLimits = true;
this.limitsMessage = "";
try {
const resp = await fetchEndorserRateLimits(
const did = this.activeDid;
if (!did) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
return;
}
await this.$saveUserSettings(did, {
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
webPushServer: this.webPushServer,
});
const imageResp = await fetchImageRateLimits(this.axios, did);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
await this.$saveSettings({
profileImageUrl: "",
});
this.profileImageUrl = "";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
return;
}
const endorserResp = await fetchEndorserRateLimits(
this.apiServer,
this.axios,
did,
);
if (resp.status === 200) {
this.endorserLimits = resp.data;
if (!this.isRegistered) {
// the user was not known to be registered, but now they are (because we got no error) so let's record it
try {
await databaseUtil.updateDidSpecificSettings(did, {
isRegistered: true,
});
this.isRegistered = true;
} catch (err) {
logger.error("Got an error updating settings:", err);
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.SETTINGS_UPDATE_ERROR,
);
}
}
try {
const imageResp = await fetchImageRateLimits(this.axios, did);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
}
} catch {
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES;
}
if (endorserResp.status === 200) {
this.endorserLimits = endorserResp.data;
} else {
await this.$saveSettings({
profileImageUrl: "",
});
this.profileImageUrl = "";
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
return;
}
} catch (error) {
this.handleRateLimitsError(error);
}
this.loadingLimits = false;
}
/**
* Handles errors that occur while fetching rate limits.
*
* @param {AxiosError | Error} error - The error object.
*/
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
logger.log(
"Got 400 or 404 response retrieving limits which probably means they're not registered:",
error,
);
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
} else {
const data = error.response?.data as ErrorResponse;
this.limitsMessage =
(data?.error?.message as string) ||
ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE;
logger.error("Got bad response retrieving limits:", error);
}
} else {
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
logger.error("Got some error retrieving limits:", error);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS);
} finally {
this.loadingLimits = false;
}
}
async onClickSaveApiServer(): Promise<void> {
await databaseUtil.updateDefaultSettings({
await this.$saveSettings({
apiServer: this.apiServerInput,
});
this.apiServer = this.apiServerInput;
}
async onClickSavePartnerServer(): Promise<void> {
await databaseUtil.updateDefaultSettings({
await this.$saveSettings({
partnerApiServer: this.partnerApiServerInput,
});
this.partnerApiServer = this.partnerApiServerInput;
await this.$saveUserSettings(this.activeDid, {
partnerApiServer: this.partnerApiServer,
});
}
async onClickSavePushServer(): Promise<void> {
await databaseUtil.updateDefaultSettings({
await this.$saveSettings({
webPushServer: this.webPushServerInput,
});
this.webPushServer = this.webPushServerInput;
@@ -1735,7 +1659,7 @@ export default class AccountViewView extends Vue {
openImageDialog(): void {
(this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => {
await databaseUtil.updateDefaultSettings({
await this.$saveSettings({
profileImageUrl: imgUrl,
});
this.profileImageUrl = imgUrl;
@@ -1754,52 +1678,24 @@ export default class AccountViewView extends Vue {
}
async deleteImage(): Promise<void> {
if (!this.profileImageUrl) {
return;
}
try {
const headers = await getHeaders(this.activeDid);
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
if (
window.location.hostname === "localhost" &&
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
) {
logger.log(
"Using shared image API server, so only users on that server can play with images.",
);
}
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent(this.profileImageUrl),
{ headers },
this.apiServer + "/api/image/" + this.profileImageUrl,
{ headers: await getHeaders(this.activeDid) },
);
if (response.status === 204) {
// don't bother with a notification
// (either they'll simply continue or they're canceling and going back)
this.profileImageUrl = "";
await this.$saveSettings({
profileImageUrl: "",
});
this.notify.success("Image deleted successfully.");
} else {
logger.error("Non-success deleting image:", response);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.IMAGE_DELETE_PROBLEM);
// keep the imageUrl in localStorage so the user can try again if they want
}
await databaseUtil.updateDefaultSettings({
profileImageUrl: undefined,
});
this.profileImageUrl = undefined;
} catch (error) {
logger.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).response.status === 404) {
logger.error("The image was already deleted:", error);
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
profileImageUrl: undefined,
});
this.profileImageUrl = undefined;
if (isApiError(error) && error.response?.status === 404) {
// it already doesn't exist so we won't say anything to the user
} else {
this.notify.error(
@@ -1825,55 +1721,39 @@ export default class AccountViewView extends Vue {
async saveProfile(): Promise<void> {
this.savingProfile = true;
const profileData: ProfileData = {
description: this.userProfileDesc,
latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude,
includeLocation: this.includeUserProfileLocation,
};
try {
const headers = await getHeaders(this.activeDid);
const payload: UserProfile = {
description: this.userProfileDesc,
};
if (this.userProfileLatitude && this.userProfileLongitude) {
payload.locLat = this.userProfileLatitude;
payload.locLon = this.userProfileLongitude;
} else if (this.includeUserProfileLocation) {
this.notify.toast(
"",
ACCOUNT_VIEW_CONSTANTS.INFO.NO_PROFILE_LOCATION,
TIMEOUTS.STANDARD,
);
}
const response = await this.axios.post(
this.partnerApiServer + "/api/partner/userProfile",
payload,
{ headers },
const success = await this.profileService.saveProfile(
this.activeDid,
profileData,
);
if (response.status === 201) {
this.notify.success(
ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED,
TIMEOUTS.STANDARD,
);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
} else {
// won't get here because axios throws an error on non-success
throw Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
}
} catch (error) {
databaseUtil.logConsoleAndDb(
"Error saving profile: " + errorStringForLog(error),
);
const errorMessage: string =
extractErrorMessage(error) ||
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR;
this.notify.error(errorMessage, TIMEOUTS.STANDARD);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
} finally {
this.savingProfile = false;
}
}
toggleUserProfileLocation(): void {
this.includeUserProfileLocation = !this.includeUserProfileLocation;
if (!this.includeUserProfileLocation) {
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
this.zoom = 2;
}
const updated = this.profileService.toggleProfileLocation({
description: this.userProfileDesc,
latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude,
includeLocation: this.includeUserProfileLocation,
});
this.userProfileLatitude = updated.latitude;
this.userProfileLongitude = updated.longitude;
this.includeUserProfileLocation = updated.includeLocation;
}
confirmEraseLatLong(): void {
@@ -1900,35 +1780,19 @@ export default class AccountViewView extends Vue {
}
async deleteProfile(): Promise<void> {
this.savingProfile = true;
try {
const headers = await getHeaders(this.activeDid);
const response = await this.axios.delete(
this.partnerApiServer + "/api/partner/userProfile",
{ headers },
);
if (response.status === 204) {
const success = await this.profileService.deleteProfile(this.activeDid);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
this.userProfileDesc = "";
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
this.includeUserProfileLocation = false;
this.notify.success(
ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED,
TIMEOUTS.STANDARD,
);
} else {
throw Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_DELETED);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
}
} catch (error) {
databaseUtil.logConsoleAndDb(
"Error deleting profile: " + errorStringForLog(error),
);
const errorMessage: string =
extractErrorMessage(error) ||
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR;
this.notify.error(errorMessage, TIMEOUTS.STANDARD);
} finally {
this.savingProfile = false;
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
}
}