Browse Source

fix(profile): resolve map loading and profile deletion issues

- Fix Leaflet icon initialization error causing "Cannot read properties of undefined (reading 'Default')"
- Add proper Leaflet icon configuration with CDN fallbacks
- Implement map ready state management to prevent infinite loading
- Add comprehensive error handling and debugging for map lifecycle events
- Fix profile deletion treating HTTP 204 (No Content) as error instead of success
- Enhance error logging and user feedback throughout profile operations
- Add fallback timeout mechanisms for map initialization failures
- Improve error messages to show specific API failure reasons

Resolves map rendering issues and profile deletion failures by properly
handling HTTP status codes and Leaflet component initialization.
profile_include_location
Matthew Raymer 4 days ago
parent
commit
bc9d3cdda5
  1. 50
      src/services/ProfileService.ts
  2. 175
      src/views/AccountViewView.vue

50
src/services/ProfileService.ts

@ -124,17 +124,55 @@ export class ProfileService {
async deleteProfile(activeDid: string): Promise<boolean> { async deleteProfile(activeDid: string): Promise<boolean> {
try { try {
const headers = await getHeaders(activeDid); const headers = await getHeaders(activeDid);
const response = await this.axios.delete( logger.debug("Attempting to delete profile for DID:", activeDid);
`${this.partnerApiServer}/api/partner/userProfile`, logger.debug("Using partner API server:", this.partnerApiServer);
{ headers }, logger.debug("Request headers:", headers);
);
if (response.status === 200) { const url = `${this.partnerApiServer}/api/partner/userProfile`;
logger.debug("DELETE request URL:", url);
const response = await this.axios.delete(url, { headers });
if (response.status === 200 || response.status === 204) {
logger.debug("Profile deleted successfully");
return true; return true;
} else { } else {
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_DELETED); logger.error("Unexpected response status when deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data
});
throw new Error(`Profile not deleted - HTTP ${response.status}: ${response.statusText}`);
} }
} catch (error) { } catch (error) {
if (this.isApiError(error) && error.response) {
const response = error.response as any; // Type assertion for error response
logger.error("API error deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
url: (error as any).config?.url
});
// Handle specific HTTP status codes
if (response.status === 204) {
logger.debug("Profile deleted successfully (204 No Content)");
return true; // 204 is success for DELETE operations
} else if (response.status === 404) {
logger.warn("Profile not found - may already be deleted");
return true; // Consider this a success if profile doesn't exist
} else if (response.status === 400) {
logger.error("Bad request when deleting profile:", response.data);
throw new Error(`Profile deletion failed: ${response.data?.message || 'Bad request'}`);
} else if (response.status === 401) {
logger.error("Unauthorized to delete profile");
throw new Error("You are not authorized to delete this profile");
} else if (response.status === 403) {
logger.error("Forbidden to delete profile");
throw new Error("You are not allowed to delete this profile");
}
}
logger.error("Error deleting profile:", errorStringForLog(error)); logger.error("Error deleting profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile"); handleApiError(error as AxiosError, "/api/partner/userProfile");
return false; return false;

175
src/views/AccountViewView.vue

@ -174,14 +174,16 @@
:aria-busy="loadingProfile || savingProfile" :aria-busy="loadingProfile || savingProfile"
></textarea> ></textarea>
<div class="flex items-center mb-4" @click="toggleUserProfileLocation"> <div class="flex items-center mb-4">
<input <input
v-model="includeUserProfileLocation" v-model="includeUserProfileLocation"
type="checkbox" type="checkbox"
class="mr-2" class="mr-2"
/> @change="onLocationCheckboxChange"
<label for="includeUserProfileLocation">Include Location</label> />
</div> <label for="includeUserProfileLocation">Include Location</label>
<span class="text-xs text-slate-400 ml-2">(Debug: {{ isMapReady ? 'Map Ready' : 'Map Loading' }})</span>
</div>
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video"> <div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
<p class="text-sm mb-2 text-slate-500"> <p class="text-sm mb-2 text-slate-500">
The location you choose will be shared with the world until you remove The location you choose will be shared with the world until you remove
@ -194,6 +196,7 @@
class="!z-40 rounded-md" class="!z-40 rounded-md"
@click="onProfileMapClick" @click="onProfileMapClick"
@ready="onMapReady" @ready="onMapReady"
@mounted="onMapMounted"
> >
<l-tile-layer <l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
@ -751,6 +754,7 @@ import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there // @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import"; import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet"; import { LeafletMouseEvent } from "leaflet";
import * as L from "leaflet";
import * as R from "ramda"; import * as R from "ramda";
import { IIdentifier } from "@veramo/core"; import { IIdentifier } from "@veramo/core";
import { ref } from "vue"; import { ref } from "vue";
@ -902,6 +906,7 @@ export default class AccountViewView extends Vue {
warnIfProdServer: boolean = false; warnIfProdServer: boolean = false;
warnIfTestServer: boolean = false; warnIfTestServer: boolean = false;
zoom: number = 2; zoom: number = 2;
isMapReady: boolean = false;
// Limits and validation properties // Limits and validation properties
endorserLimits: EndorserRateLimits | null = null; endorserLimits: EndorserRateLimits | null = null;
@ -913,6 +918,17 @@ export default class AccountViewView extends Vue {
created() { created() {
this.notify = createNotifyHelpers(this.$notify); this.notify = createNotifyHelpers(this.$notify);
// Fix Leaflet icon issues in modern bundlers
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
if (L.Icon.Default) {
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png',
iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
});
}
} }
/** /**
@ -939,10 +955,16 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = profile.latitude; this.userProfileLatitude = profile.latitude;
this.userProfileLongitude = profile.longitude; this.userProfileLongitude = profile.longitude;
this.includeUserProfileLocation = profile.includeLocation; this.includeUserProfileLocation = profile.includeLocation;
// Initialize map ready state if location is included
if (profile.includeLocation) {
this.isMapReady = false; // Will be set to true when map is ready
}
} else { } else {
// Profile not created yet; leave defaults // Profile not created yet; leave defaults
} }
} catch (error) { } catch (error) {
logger.error("Error loading profile:", error);
this.notify.error( this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE, ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_AVAILABLE,
); );
@ -1518,9 +1540,45 @@ export default class AccountViewView extends Vue {
} }
onMapReady(map: L.Map): void { 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 try {
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2; logger.debug("Map ready event fired, map object:", map);
map.setView([this.userProfileLatitude, this.userProfileLongitude], zoom); // doing this here instead of on the l-map element avoids a recentering after a drag then zoom at startup
const zoom = this.userProfileLatitude && this.userProfileLongitude ? 12 : 2;
const lat = this.userProfileLatitude || 0;
const lng = this.userProfileLongitude || 0;
map.setView([lat, lng], zoom);
this.isMapReady = true;
logger.debug("Map ready state set to true, coordinates:", [lat, lng], "zoom:", zoom);
} catch (error) {
logger.error("Error in onMapReady:", error);
this.isMapReady = true; // Set to true even on error to prevent infinite loading
}
}
onMapMounted(): void {
logger.debug("Map component mounted");
// Check if map ref is available
const mapRef = this.$refs.profileMap;
logger.debug("Map ref:", mapRef);
// Try to set map ready after component is mounted
setTimeout(() => {
this.isMapReady = true;
logger.debug("Map ready set to true after mounted");
}, 500);
}
// Fallback method to handle map initialization failures
private handleMapInitFailure(): void {
logger.debug("Starting map initialization timeout (5 seconds)");
setTimeout(() => {
if (!this.isMapReady) {
logger.warn("Map failed to initialize, forcing ready state");
this.isMapReady = true;
} else {
logger.debug("Map initialized successfully, timeout not needed");
}
}, 5000); // 5 second timeout
} }
showProfileInfo(): void { showProfileInfo(): void {
@ -1532,13 +1590,16 @@ export default class AccountViewView extends Vue {
async saveProfile(): Promise<void> { async saveProfile(): Promise<void> {
this.savingProfile = true; this.savingProfile = true;
const profileData: ProfileData = {
description: this.userProfileDesc,
latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude,
includeLocation: this.includeUserProfileLocation,
};
try { try {
const profileData: ProfileData = {
description: this.userProfileDesc,
latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude,
includeLocation: this.includeUserProfileLocation,
};
logger.debug("Saving profile data:", profileData);
const success = await this.profileService.saveProfile( const success = await this.profileService.saveProfile(
this.activeDid, this.activeDid,
profileData, profileData,
@ -1549,6 +1610,7 @@ export default class AccountViewView extends Vue {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
} }
} catch (error) { } catch (error) {
logger.error("Error saving profile:", error);
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
} finally { } finally {
this.savingProfile = false; this.savingProfile = false;
@ -1556,15 +1618,25 @@ export default class AccountViewView extends Vue {
} }
toggleUserProfileLocation(): void { toggleUserProfileLocation(): void {
const updated = this.profileService.toggleProfileLocation({ try {
description: this.userProfileDesc, const updated = this.profileService.toggleProfileLocation({
latitude: this.userProfileLatitude, description: this.userProfileDesc,
longitude: this.userProfileLongitude, latitude: this.userProfileLatitude,
includeLocation: this.includeUserProfileLocation, longitude: this.userProfileLongitude,
}); includeLocation: this.includeUserProfileLocation,
this.userProfileLatitude = updated.latitude; });
this.userProfileLongitude = updated.longitude; this.userProfileLatitude = updated.latitude;
this.includeUserProfileLocation = updated.includeLocation; this.userProfileLongitude = updated.longitude;
this.includeUserProfileLocation = updated.includeLocation;
// Reset map ready state when toggling location
if (!updated.includeLocation) {
this.isMapReady = false;
}
} catch (error) {
logger.error("Error in toggleUserProfileLocation:", error);
this.notify.error("Failed to toggle location setting");
}
} }
confirmEraseLatLong(): void { confirmEraseLatLong(): void {
@ -1592,6 +1664,7 @@ export default class AccountViewView extends Vue {
async deleteProfile(): Promise<void> { async deleteProfile(): Promise<void> {
try { try {
logger.debug("Attempting to delete profile for DID:", this.activeDid);
const success = await this.profileService.deleteProfile(this.activeDid); const success = await this.profileService.deleteProfile(this.activeDid);
if (success) { if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED); this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
@ -1599,11 +1672,20 @@ export default class AccountViewView extends Vue {
this.userProfileLatitude = 0; this.userProfileLatitude = 0;
this.userProfileLongitude = 0; this.userProfileLongitude = 0;
this.includeUserProfileLocation = false; this.includeUserProfileLocation = false;
this.isMapReady = false; // Reset map state
logger.debug("Profile deleted successfully, UI state reset");
} else { } else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR); this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
} }
} catch (error) { } catch (error) {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR); logger.error("Error in deleteProfile component method:", error);
// Show more specific error message if available
if (error instanceof Error) {
this.notify.error(error.message);
} else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
}
} }
} }
@ -1616,8 +1698,43 @@ export default class AccountViewView extends Vue {
} }
onProfileMapClick(event: LeafletMouseEvent) { onProfileMapClick(event: LeafletMouseEvent) {
this.userProfileLatitude = event.latlng.lat; try {
this.userProfileLongitude = event.latlng.lng; if (event && event.latlng) {
this.userProfileLatitude = event.latlng.lat;
this.userProfileLongitude = event.latlng.lng;
}
} catch (error) {
logger.error("Error in onProfileMapClick:", error);
}
}
onLocationCheckboxChange(): void {
try {
logger.debug("Location checkbox changed, new value:", this.includeUserProfileLocation);
if (!this.includeUserProfileLocation) {
// Location checkbox was unchecked, clean up map state
this.isMapReady = false;
this.userProfileLatitude = 0;
this.userProfileLongitude = 0;
logger.debug("Location unchecked, map state reset");
} else {
// Location checkbox was checked, start map initialization timeout
this.isMapReady = false;
logger.debug("Location checked, starting map initialization timeout");
// Try to set map ready after a short delay to allow Vue to render
setTimeout(() => {
if (!this.isMapReady) {
logger.debug("Setting map ready after timeout");
this.isMapReady = true;
}
}, 1000); // 1 second delay
this.handleMapInitFailure();
}
} catch (error) {
logger.error("Error in onLocationCheckboxChange:", error);
}
} }
// IdentitySection event handlers // IdentitySection event handlers

Loading…
Cancel
Save