Merge branch 'master' into account-import-duplicate-prevention

This commit is contained in:
Jose Olarte III
2025-09-01 18:06:36 +08:00
35 changed files with 3610 additions and 610 deletions

View File

@@ -58,8 +58,10 @@
v-if="!isRegistered"
:passkeys-enabled="PASSKEYS_ENABLED"
:given-name="givenName"
message="Before you can publicly announce a new project or time commitment,
a friend needs to register you."
:message="
`Before you can publicly announce a new project or time commitment, ` +
`a friend needs to register you.`
"
/>
<!-- Notifications -->
@@ -752,6 +754,7 @@ import "leaflet/dist/leaflet.css";
import { Buffer } from "buffer/";
import "dexie-export-import";
// @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
@@ -813,11 +816,13 @@ import {
isApiError,
ImportContent,
} from "@/interfaces/accountView";
import {
ProfileService,
createProfileService,
ProfileData,
} from "@/services/ProfileService";
// Profile data interface (inlined from ProfileService)
interface ProfileData {
description: string;
latitude: number;
longitude: number;
includeLocation: boolean;
}
const inputImportFileNameRef = ref<Blob>();
@@ -916,7 +921,6 @@ export default class AccountViewView extends Vue {
imageLimits: ImageRateLimits | null = null;
limitsMessage: string = "";
private profileService!: ProfileService;
private notify!: ReturnType<typeof createNotifyHelpers>;
created() {
@@ -926,7 +930,10 @@ export default class AccountViewView extends Vue {
// This prevents the "Cannot read properties of undefined (reading 'Default')" error
if (L.Icon.Default) {
// Type-safe way to handle Leaflet icon prototype
const iconDefault = L.Icon.Default.prototype as Record<string, unknown>;
const iconDefault = L.Icon.Default.prototype as unknown as Record<
string,
unknown
>;
if ("_getIconUrl" in iconDefault) {
delete iconDefault._getIconUrl;
}
@@ -948,17 +955,21 @@ export default class AccountViewView extends Vue {
* @throws Will display specific messages to the user based on different errors.
*/
async mounted(): Promise<void> {
this.profileService = createProfileService(
this.axios,
this.partnerApiServer,
);
try {
await this.initializeState();
await this.processIdentity();
// Profile service logic now inlined - no need for external service
logger.debug(
"[AccountViewView] Profile logic ready with partnerApiServer:",
{
partnerApiServer: this.partnerApiServer,
},
);
if (this.isRegistered) {
try {
const profile = await this.profileService.loadProfile(this.activeDid);
const profile = await this.loadProfile(this.activeDid);
if (profile) {
this.userProfileDesc = profile.description;
this.userProfileLatitude = profile.latitude;
@@ -1411,21 +1422,24 @@ export default class AccountViewView extends Vue {
async checkLimits(): Promise<void> {
this.loadingLimits = true;
const did = this.activeDid;
if (!did) {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IDENTIFIER;
return;
}
try {
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);
const imageResp = await fetchImageRateLimits(
this.axios,
did,
this.DEFAULT_IMAGE_API_SERVER,
);
if (imageResp.status === 200) {
this.imageLimits = imageResp.data;
@@ -1451,7 +1465,26 @@ export default class AccountViewView extends Vue {
} catch (error) {
this.limitsMessage =
ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS;
logger.error("Error retrieving limits: ", error);
// Enhanced error logging with server context
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.error("[Server Limits] Error retrieving limits:", {
error: error instanceof Error ? error.message : String(error),
did: did,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
errorCode: axiosError?.response?.data?.error?.code,
errorMessage: axiosError?.response?.data?.error?.message,
httpStatus: axiosError?.response?.status,
needsUserMigration: true,
timestamp: new Date().toISOString(),
});
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
} finally {
this.loadingLimits = false;
@@ -1459,24 +1492,70 @@ export default class AccountViewView extends Vue {
}
async onClickSaveApiServer(): Promise<void> {
await this.$saveSettings({
apiServer: this.apiServerInput,
// Enhanced diagnostic logging for claim URL changes
const previousApiServer = this.apiServer;
const newApiServer = this.apiServerInput;
logger.debug("[Server Switching] Claim URL change initiated:", {
did: this.activeDid,
previousServer: previousApiServer,
newServer: newApiServer,
changeType: "apiServer",
timestamp: new Date().toISOString(),
});
this.apiServer = this.apiServerInput;
await this.$saveSettings({
apiServer: newApiServer,
});
this.apiServer = newApiServer;
// Add this line to save to user-specific settings
await this.$saveUserSettings(this.activeDid, {
apiServer: this.apiServer,
});
// Log successful server switch
logger.debug("[Server Switching] Claim URL change completed:", {
did: this.activeDid,
previousServer: previousApiServer,
newServer: newApiServer,
changeType: "apiServer",
settingsSaved: true,
timestamp: new Date().toISOString(),
});
}
async onClickSavePartnerServer(): Promise<void> {
await this.$saveSettings({
partnerApiServer: this.partnerApiServerInput,
// Enhanced diagnostic logging for partner server changes
const previousPartnerServer = this.partnerApiServer;
const newPartnerServer = this.partnerApiServerInput;
logger.debug("[Server Switching] Partner server change initiated:", {
did: this.activeDid,
previousServer: previousPartnerServer,
newServer: newPartnerServer,
changeType: "partnerApiServer",
timestamp: new Date().toISOString(),
});
this.partnerApiServer = this.partnerApiServerInput;
await this.$saveSettings({
partnerApiServer: newPartnerServer,
});
this.partnerApiServer = newPartnerServer;
await this.$saveUserSettings(this.activeDid, {
partnerApiServer: this.partnerApiServer,
});
// Log successful partner server switch
logger.debug("[Server Switching] Partner server change completed:", {
did: this.activeDid,
previousServer: previousPartnerServer,
newServer: newPartnerServer,
changeType: "partnerApiServer",
settingsSaved: true,
timestamp: new Date().toISOString(),
});
}
async onClickSavePushServer(): Promise<void> {
@@ -1550,7 +1629,6 @@ export default class AccountViewView extends Vue {
onMapReady(map: L.Map): void {
try {
logger.debug("Map ready event fired, map object:", map);
// 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;
@@ -1579,19 +1657,15 @@ export default class AccountViewView extends Vue {
// 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
}
@@ -1615,7 +1689,7 @@ export default class AccountViewView extends Vue {
logger.debug("Saving profile data:", profileData);
const success = await this.profileService.saveProfile(
const success = await this.saveProfileToServer(
this.activeDid,
profileData,
);
@@ -1634,7 +1708,7 @@ export default class AccountViewView extends Vue {
toggleUserProfileLocation(): void {
try {
const updated = this.profileService.toggleProfileLocation({
const updated = this.toggleProfileLocation({
description: this.userProfileDesc,
latitude: this.userProfileLatitude,
longitude: this.userProfileLongitude,
@@ -1679,8 +1753,7 @@ export default class AccountViewView extends Vue {
async deleteProfile(): Promise<void> {
try {
logger.debug("Attempting to delete profile for DID:", this.activeDid);
const success = await this.profileService.deleteProfile(this.activeDid);
const success = await this.deleteProfileFromServer(this.activeDid);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_DELETED);
this.userProfileDesc = "";
@@ -1688,7 +1761,6 @@ export default class AccountViewView extends Vue {
this.userProfileLongitude = 0;
this.includeUserProfileLocation = false;
this.isMapReady = false; // Reset map state
logger.debug("Profile deleted successfully, UI state reset");
} else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_DELETE_ERROR);
}
@@ -1734,7 +1806,6 @@ export default class AccountViewView extends Vue {
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;
@@ -1743,7 +1814,6 @@ export default class AccountViewView extends Vue {
// 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
@@ -1796,5 +1866,338 @@ export default class AccountViewView extends Vue {
onRecheckLimits() {
this.checkLimits();
}
// Inlined profile methods (previously in ProfileService)
/**
* Load user profile from the partner API
*/
private async loadProfile(did: string): Promise<ProfileData | null> {
try {
const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Loading profile:", {
requestId,
did,
partnerApiServer: this.partnerApiServer,
});
// Get authentication headers
const headers = await getHeaders(did);
const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`;
logger.debug("[AccountViewView] Making API request:", {
requestId,
did,
fullUrl,
hasAuthHeader: !!headers.Authorization,
});
const response = await this.axios.get(fullUrl, { headers });
logger.debug("[AccountViewView] Profile loaded successfully:", {
requestId,
status: response.status,
hasData: !!response.data,
});
if (response.data && response.data.data) {
const profileData = response.data.data;
logger.debug("[AccountViewView] Parsing profile data:", {
requestId,
locLat: profileData.locLat,
locLon: profileData.locLon,
description: profileData.description,
});
const result = {
description: profileData.description || "",
latitude: profileData.locLat || 0,
longitude: profileData.locLon || 0,
includeLocation: !!(profileData.locLat && profileData.locLon),
};
logger.debug("[AccountViewView] Parsed profile result:", {
requestId,
result,
hasLocation: result.includeLocation,
});
return result;
} else {
logger.debug("[AccountViewView] No profile data found in response:", {
requestId,
hasData: !!response.data,
hasDataData: !!(response.data && response.data.data),
});
}
return null;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.info(
"[Profile] No profile found - this is normal for new users",
{
did,
server: this.partnerApiServer,
status: 404,
timestamp: new Date().toISOString(),
},
);
return null;
}
if (axiosError.response?.status === 400) {
logger.warn("[Profile] Bad request - user may not be registered", {
did,
server: this.partnerApiServer,
status: 400,
timestamp: new Date().toISOString(),
});
return null;
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn("[Profile] Authentication/authorization issue", {
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
});
return null;
}
}
// Only log full errors for unexpected issues (5xx, network errors, etc.)
logger.error("[Profile] Unexpected error loading profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
throw new Error("Failed to load profile");
}
}
/**
* Save user profile to the partner API
*/
private async saveProfileToServer(
did: string,
profileData: ProfileData,
): Promise<boolean> {
try {
const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Saving profile:", {
requestId,
did,
profileData,
});
// Get authentication headers
const headers = await getHeaders(did);
// Prepare payload in the format expected by the partner API
const payload = {
description: profileData.description,
issuerDid: did,
...(profileData.includeLocation &&
profileData.latitude &&
profileData.longitude
? {
locLat: profileData.latitude,
locLon: profileData.longitude,
}
: {}),
};
logger.debug("[AccountViewView] Sending payload to server:", {
requestId,
payload,
hasLocation: profileData.includeLocation,
});
const response = await this.axios.post(
`${this.partnerApiServer}/api/partner/userProfile`,
payload,
{ headers },
);
logger.debug("[AccountViewView] Profile saved successfully:", {
requestId,
status: response.status,
});
return true;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 400) {
logger.warn("[Profile] Bad request saving profile", {
did,
server: this.partnerApiServer,
status: 400,
timestamp: new Date().toISOString(),
});
throw new Error("Invalid profile data");
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn(
"[Profile] Authentication/authorization issue saving profile",
{
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
},
);
throw new Error("Authentication required");
}
if (axiosError.response?.status === 409) {
logger.warn("[Profile] Profile conflict - may already exist", {
did,
server: this.partnerApiServer,
status: 409,
timestamp: new Date().toISOString(),
});
throw new Error("Profile already exists");
}
}
// Only log full errors for unexpected issues
logger.error("[Profile] Unexpected error saving profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
throw new Error("Failed to save profile");
}
}
/**
* Toggle profile location visibility
*/
private toggleProfileLocation(profileData: ProfileData): ProfileData {
const includeLocation = !profileData.includeLocation;
return {
...profileData,
latitude: includeLocation ? profileData.latitude : 0,
longitude: includeLocation ? profileData.longitude : 0,
includeLocation,
};
}
/**
* Clear profile location
*/
private clearProfileLocation(profileData: ProfileData): ProfileData {
return {
...profileData,
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Get default profile data
*/
private getDefaultProfile(): ProfileData {
return {
description: "",
latitude: 0,
longitude: 0,
includeLocation: false,
};
}
/**
* Delete user profile from the partner API
*/
private async deleteProfileFromServer(did: string): Promise<boolean> {
try {
const requestId = `profile_delete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.debug("[AccountViewView] Deleting profile:", {
requestId,
did,
});
// Get authentication headers
const headers = await getHeaders(did);
const response = await this.axios.delete(
`${this.partnerApiServer}/api/partner/userProfile/${did}`,
{ headers },
);
logger.debug("[AccountViewView] Profile deleted successfully:", {
requestId,
status: response.status,
});
return true;
} catch (error: unknown) {
// Handle specific HTTP status codes cleanly to suppress console spam
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 404) {
logger.info(
"[Profile] Profile not found for deletion - may already be deleted",
{
did,
server: this.partnerApiServer,
status: 404,
timestamp: new Date().toISOString(),
},
);
return true; // Consider it successful if already deleted
}
if (
axiosError.response?.status === 401 ||
axiosError.response?.status === 403
) {
logger.warn(
"[Profile] Authentication/authorization issue deleting profile",
{
did,
server: this.partnerApiServer,
status: axiosError.response.status,
timestamp: new Date().toISOString(),
},
);
return false;
}
}
// Only log full errors for unexpected issues
logger.error("[Profile] Unexpected error deleting profile:", {
did,
server: this.partnerApiServer,
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
return false;
}
}
}
</script>

View File

@@ -124,7 +124,7 @@ import {
NOTIFY_CONFIRMATION_RESTRICTION,
} from "../constants/notifications";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { GiveSummaryRecord, GiveActionClaim } from "../interfaces";
import { AgreeActionClaim } from "../interfaces/claims";
import {
@@ -223,7 +223,7 @@ export default class ContactAmountssView extends Vue {
const contact = await this.$getContact(contactDid);
this.contact = contact;
const settings = await this.$getSettings(MASTER_SETTINGS_KEY);
const settings = await this.$getMasterSettings();
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";

View File

@@ -568,10 +568,27 @@ export default class HomeView extends Vue {
this.isRegistered = true;
}
} catch (error) {
// Enhanced error logging with server context
const errorMessage =
error instanceof Error ? error.message : String(error);
const axiosError = error as {
response?: {
data?: { error?: { code?: string; message?: string } };
status?: number;
};
};
logger.warn(
"[HomeView Settings Trace] ⚠️ Registration check failed",
{
error: error instanceof Error ? error.message : String(error),
error: errorMessage,
did: this.activeDid,
server: this.apiServer,
errorCode: axiosError?.response?.data?.error?.code,
errorMessage: axiosError?.response?.data?.error?.message,
httpStatus: axiosError?.response?.status,
needsUserMigration: true,
timestamp: new Date().toISOString(),
},
);
}
@@ -585,8 +602,7 @@ export default class HomeView extends Vue {
}
/**
* Ensures API server is correctly set for the current platform
* For Electron, always use production endpoint regardless of saved settings
* Ensures correct API server configuration
*
* @internal
* Called after loading settings to ensure correct API endpoint
@@ -594,12 +610,9 @@ export default class HomeView extends Vue {
private async ensureCorrectApiServer() {
const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app");
if (process.env.VITE_PLATFORM === "electron") {
// **CRITICAL FIX**: Always use production API server for Electron
// This prevents the capacitor-electron:// protocol from being used for API calls
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
} else if (!this.apiServer) {
// **FIX**: Set default API server for web/development if not already set
// Only set default if no user preference exists
if (!this.apiServer) {
// Set default API server for any platform if not already set
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
}
@@ -1161,9 +1174,13 @@ export default class HomeView extends Vue {
location: fulfillsPlan
? { lat: fulfillsPlan.locLat, lon: fulfillsPlan.locLon }
: null,
inSearchBox: fulfillsPlan
? this.latLongInAnySearchBox(fulfillsPlan.locLat, fulfillsPlan.locLon)
: null,
inSearchBox:
fulfillsPlan?.locLat && fulfillsPlan?.locLon
? this.latLongInAnySearchBox(
fulfillsPlan.locLat,
fulfillsPlan.locLon,
)
: null,
finalResult: anyMatch,
});
}

View File

@@ -229,7 +229,7 @@ export default class IdentitySwitcherView extends Vue {
if (did) {
try {
const newSettings = await this.$accountSettings(did);
logger.info(
logger.debug(
"[IdentitySwitcher Settings Trace] ✅ New account settings loaded",
{
did,
@@ -252,7 +252,7 @@ export default class IdentitySwitcherView extends Vue {
}
}
logger.info(
logger.debug(
"[IdentitySwitcher Settings Trace] 🔄 Navigating to home to trigger watcher",
{
newDid: did,

View File

@@ -110,10 +110,22 @@ export default class NewEditAccountView extends Vue {
* @async
*/
async onClickSaveChanges() {
await this.$updateSettings({
firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3
});
// Get the current active DID to save to user-specific settings
const settings = await this.$accountSettings();
const activeDid = settings.activeDid;
if (activeDid) {
// Save to user-specific settings for the current identity
await this.$saveUserSettings(activeDid, {
firstName: this.givenName,
});
} else {
// Fallback to master settings if no active DID
await this.$saveSettings({
firstName: this.givenName,
});
}
this.$router.back();
}

View File

@@ -203,7 +203,7 @@ export default class StartView extends Vue {
// Load account count for display logic
this.numAccounts = await retrieveAccountCount();
logger.info("[StartView] Component mounted", {
logger.debug("[StartView] Component mounted", {
hasGivenName: !!this.givenName,
accountCount: this.numAccounts,
passkeysEnabled: this.PASSKEYS_ENABLED,
@@ -221,7 +221,7 @@ export default class StartView extends Vue {
* Routes user to new identifier creation flow with seed-based approach
*/
public onClickNewSeed() {
logger.info("[StartView] User selected new seed generation");
logger.debug("[StartView] User selected new seed generation");
this.$router.push({ name: "new-identifier" });
}
@@ -235,14 +235,14 @@ export default class StartView extends Vue {
const keyName =
AppString.APP_NAME + (this.givenName ? " - " + this.givenName : "");
logger.info("[StartView] Initiating passkey registration", {
logger.debug("[StartView] Initiating passkey registration", {
keyName,
hasGivenName: !!this.givenName,
});
await registerSaveAndActivatePasskey(keyName);
logger.info("[StartView] Passkey registration successful");
logger.debug("[StartView] Passkey registration successful");
this.$router.push({ name: "account" });
} catch (error) {
logger.error("[StartView] Passkey registration failed", error);
@@ -255,7 +255,7 @@ export default class StartView extends Vue {
* Routes user to account import flow for existing seed phrase
*/
public onClickNo() {
logger.info("[StartView] User selected existing seed import");
logger.debug("[StartView] User selected existing seed import");
this.$router.push({ name: "import-account" });
}
@@ -264,7 +264,7 @@ export default class StartView extends Vue {
* Routes user to address derivation flow for existing seed
*/
public onClickDerive() {
logger.info("[StartView] User selected address derivation");
logger.debug("[StartView] User selected address derivation");
this.$router.push({ name: "import-derive" });
}
}

View File

@@ -91,13 +91,95 @@
name: 'shared-photo',
query: { fileName },
}"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
data-testId="fileUploadButton"
>
Go to Shared Page
</router-link>
</div>
<!-- URL Flow Testing Section -->
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">URL Flow Testing</h2>
<p class="text-sm text-gray-600 mb-3">
Test claim and partner server URL flow from initialization to change
propagation.
</p>
<div class="space-y-4">
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">Current URL State</h3>
<div class="space-y-2 text-sm">
<div>
<strong>API Server:</strong>
<span class="font-mono">{{ apiServer || "Not Set" }}</span>
</div>
<div>
<strong>Partner API Server:</strong>
<span class="font-mono">{{ partnerApiServer || "Not Set" }}</span>
</div>
<div>
<strong>Active DID:</strong>
<span class="font-mono">{{ activeDid || "Not Set" }}</span>
</div>
<div>
<strong>Platform:</strong>
<span class="font-mono">{{ getCurrentPlatform() }}</span>
</div>
</div>
</div>
<div class="space-y-2">
<button
:class="primaryButtonClasses"
:disabled="isUrlTestRunning"
@click="testUrlFlow()"
>
{{ isUrlTestRunning ? "Testing..." : "Test URL Flow" }}
</button>
<button :class="secondaryButtonClasses" @click="changeApiServer()">
Change API Server (Test Prod)
</button>
<button
:class="secondaryButtonClasses"
@click="changePartnerApiServer()"
>
Change Partner API Server (Test Prod)
</button>
<button :class="warningButtonClasses" @click="resetToDefaults()">
Reset to Defaults
</button>
<button :class="secondaryButtonClasses" @click="refreshSettings()">
Refresh Settings
</button>
<button
:class="secondaryButtonClasses"
@click="logEnvironmentState()"
>
Log Environment State
</button>
</div>
<div class="p-4 border border-gray-300 rounded-md bg-gray-50">
<h3 class="font-semibold mb-2">URL Flow Test Results</h3>
<div class="max-h-64 overflow-y-auto space-y-2">
<div
v-for="(result, index) in urlTestResults"
:key="index"
class="p-2 border border-gray-200 rounded text-xs font-mono bg-white"
>
{{ result }}
</div>
</div>
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2>
See console for results.
@@ -326,6 +408,11 @@ export default class Help extends Vue {
showEntityGridTest = false;
showPlatformServiceTest = false;
// for URL flow testing
isUrlTestRunning = false;
urlTestResults: string[] = [];
partnerApiServer: string | undefined;
/**
* Computed properties for template streamlining
* Eliminates repeated classes and logic in template
@@ -534,24 +621,93 @@ export default class Help extends Vue {
}
/**
* Component initialization
*
* Loads user settings and account information for testing interface
* Uses PlatformServiceMixin for database access
*/
async mounted() {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.userName = settings.firstName;
logger.info(
"[TestView] 🚀 Component mounting - starting URL flow tracking",
);
const account = await retrieveAccountMetadata(this.activeDid);
if (this.activeDid) {
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
} else {
alert("No account found for DID " + this.activeDid);
// Boot-time logging for initial configuration
logger.info("[TestView] 🌍 Boot-time configuration detected:", {
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
nodeEnv: process.env.NODE_ENV,
timestamp: new Date().toISOString(),
});
try {
// Track settings loading
logger.info("[TestView] 📥 Loading account settings...");
const settings = await this.$accountSettings();
logger.info("[TestView] 📊 Settings loaded:", {
activeDid: settings.activeDid,
apiServer: settings.apiServer,
partnerApiServer: settings.partnerApiServer,
isRegistered: settings.isRegistered,
firstName: settings.firstName,
});
// Update component state
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
this.userName = settings.firstName;
logger.info("[TestView] ✅ Component state updated:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
// Load account metadata
if (this.activeDid) {
logger.info(
"[TestView] 🔍 Loading account metadata for DID:",
this.activeDid,
);
const account = await retrieveAccountMetadata(this.activeDid);
if (account) {
this.credIdHex = account.passkeyCredIdHex as string;
logger.info("[TestView] ✅ Account metadata loaded:", {
did: account.did,
hasPasskey: !!account.passkeyCredIdHex,
passkeyId: account.passkeyCredIdHex,
});
} else {
logger.warn(
"[TestView] ⚠️ No account found for DID:",
this.activeDid,
);
alert("No account found for DID " + this.activeDid);
}
}
logger.info("[TestView] 🎯 Component initialization complete:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
hasPasskey: !!this.credIdHex,
platform: this.getCurrentPlatform(),
});
} catch (error) {
logger.error(
"[TestView] ❌ Error during component initialization:",
error,
);
this.$notify(
{
group: "error",
type: "error",
title: "Initialization Error",
text: `Failed to initialize component: ${error instanceof Error ? error.message : String(error)}`,
},
5000,
);
}
}
@@ -824,5 +980,276 @@ export default class Help extends Vue {
);
}
}
/**
* Tests the URL flow from initialization to change propagation.
* This simulates the flow where a user's DID is set, and then the
* claim and partner server URLs are updated.
*/
public async testUrlFlow() {
this.isUrlTestRunning = true;
this.urlTestResults = [];
try {
logger.info("[TestView] 🔬 Starting comprehensive URL flow test");
this.addUrlTestResult("🚀 Starting URL flow test...");
// Test 1: Current state
this.addUrlTestResult(`📊 Current State:`);
this.addUrlTestResult(` - API Server: ${this.apiServer || "Not Set"}`);
this.addUrlTestResult(
` - Partner API Server: ${this.partnerApiServer || "Not Set"}`,
);
this.addUrlTestResult(` - Active DID: ${this.activeDid || "Not Set"}`);
this.addUrlTestResult(` - Platform: ${this.getCurrentPlatform()}`);
// Test 2: Load fresh settings
this.addUrlTestResult(`\n📥 Testing Settings Loading:`);
const startTime = Date.now();
const settings = await this.$accountSettings();
const loadTime = Date.now() - startTime;
this.addUrlTestResult(` - Settings loaded in ${loadTime}ms`);
this.addUrlTestResult(
` - API Server from settings: ${settings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Partner API Server from settings: ${settings.partnerApiServer || "Not Set"}`,
);
// Test 3: Database query
this.addUrlTestResult(`\n💾 Testing Database Query:`);
const dbStartTime = Date.now();
const dbResult = await this.$dbQuery(
"SELECT apiServer, partnerApiServer, activeDid FROM settings WHERE id = ? OR accountDid = ?",
[1, this.activeDid || ""],
);
const dbTime = Date.now() - dbStartTime;
if (dbResult?.values) {
this.addUrlTestResult(` - Database query completed in ${dbTime}ms`);
this.addUrlTestResult(
` - Raw DB values: ${JSON.stringify(dbResult.values)}`,
);
} else {
this.addUrlTestResult(
` - Database query failed or returned no results`,
);
}
// Test 4: Environment variables
this.addUrlTestResult(`\n🌍 Testing Environment Variables:`);
this.addUrlTestResult(
` - VITE_PLATFORM: ${import.meta.env.VITE_PLATFORM || "Not Set"}`,
);
this.addUrlTestResult(
` - VITE_DEFAULT_ENDORSER_API_SERVER: ${import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER || "Not Set"}`,
);
this.addUrlTestResult(
` - VITE_DEFAULT_PARTNER_API_SERVER: ${import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER || "Not Set"}`,
);
// Test 5: Constants
this.addUrlTestResult(`\n📋 Testing App Constants:`);
this.addUrlTestResult(
` - PROD_ENDORSER_API_SERVER: ${AppString.PROD_ENDORSER_API_SERVER}`,
);
this.addUrlTestResult(
` - PROD_PARTNER_API_SERVER: ${AppString.PROD_PARTNER_API_SERVER}`,
);
// Test 6: Change detection
this.addUrlTestResult(`\n🔄 Testing Change Detection:`);
const originalApiServer = this.apiServer;
const originalPartnerServer = this.partnerApiServer;
// Simulate a change
this.addUrlTestResult(` - Original API Server: ${originalApiServer}`);
this.addUrlTestResult(
` - Original Partner Server: ${originalPartnerServer}`,
);
// Test 7: Settings update
this.addUrlTestResult(`\n💾 Testing Settings Update:`);
const testChanges = {
apiServer:
originalApiServer === "https://api.endorser.ch"
? "https://test-api.endorser.ch"
: "https://api.endorser.ch",
};
this.addUrlTestResult(
` - Attempting to change API Server to: ${testChanges.apiServer}`,
);
const updateResult = await this.$saveSettings(testChanges);
this.addUrlTestResult(
` - Update result: ${updateResult ? "Success" : "Failed"}`,
);
// Test 8: Verify change propagation
this.addUrlTestResult(`\n✅ Testing Change Propagation:`);
const newSettings = await this.$accountSettings();
this.addUrlTestResult(
` - New API Server from settings: ${newSettings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Component state API Server: ${this.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Change propagated: ${newSettings.apiServer === this.apiServer ? "Yes" : "No"}`,
);
// Test 9: Revert changes
this.addUrlTestResult(`\n🔄 Reverting Changes:`);
const revertResult = await this.$saveSettings({
apiServer: originalApiServer,
});
this.addUrlTestResult(
` - Revert result: ${revertResult ? "Success" : "Failed"}`,
);
// Test 10: Final verification
this.addUrlTestResult(`\n🎯 Final Verification:`);
const finalSettings = await this.$accountSettings();
this.addUrlTestResult(
` - Final API Server: ${finalSettings.apiServer || "Not Set"}`,
);
this.addUrlTestResult(
` - Matches original: ${finalSettings.apiServer === originalApiServer ? "Yes" : "No"}`,
);
this.addUrlTestResult(`\n✅ URL flow test completed successfully!`);
logger.info("[TestView] ✅ URL flow test completed successfully");
} catch (error) {
const errorMsg = `❌ URL flow test failed: ${error instanceof Error ? error.message : String(error)}`;
this.addUrlTestResult(errorMsg);
logger.error("[TestView] ❌ URL flow test failed:", error);
} finally {
this.isUrlTestRunning = false;
}
}
/**
* Adds a result to the URL test results array.
*/
private addUrlTestResult(message: string) {
this.urlTestResults.push(message);
}
/**
* Changes the API server to the production URL.
*/
public changeApiServer() {
const currentServer = this.apiServer;
const newServer =
currentServer === "https://api.endorser.ch"
? "https://test-api.endorser.ch"
: "https://api.endorser.ch";
logger.info("[TestView] 🔄 Changing API server:", {
from: currentServer,
to: newServer,
});
this.apiServer = newServer;
this.addUrlTestResult(
`API Server changed from ${currentServer} to ${newServer}`,
);
}
/**
* Changes the partner API server to the production URL.
*/
public changePartnerApiServer() {
const currentServer = this.partnerApiServer;
const newServer =
currentServer === "https://partner-api.endorser.ch"
? "https://test-partner-api.endorser.ch"
: "https://partner-api.endorser.ch";
logger.info("[TestView] 🔄 Changing partner API server:", {
from: currentServer,
to: newServer,
});
this.partnerApiServer = newServer;
this.addUrlTestResult(
`Partner API Server changed from ${currentServer} to ${newServer}`,
);
}
/**
* Resets all URL-related settings to their initial values.
*/
public resetToDefaults() {
this.apiServer = AppString.TEST_ENDORSER_API_SERVER;
this.partnerApiServer = AppString.TEST_PARTNER_API_SERVER;
this.activeDid = "";
this.addUrlTestResult("URL Flow Test Results Reset to Defaults.");
}
/**
* Refreshes settings from the database to verify changes.
*/
public async refreshSettings() {
try {
logger.info("[TestView] 🔄 Refreshing settings from database");
const settings = await this.$accountSettings();
// Update component state
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
logger.info("[TestView] ✅ Settings refreshed:", {
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
this.addUrlTestResult(
`Settings refreshed - API Server: ${this.apiServer}, Partner API Server: ${this.partnerApiServer}`,
);
} catch (error) {
logger.error("[TestView] ❌ Error refreshing settings:", error);
this.addUrlTestResult(
`Error refreshing settings: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Logs the current environment state to the console.
*/
public logEnvironmentState() {
logger.info("[TestView] 🌐 Current Environment State:", {
VITE_PLATFORM: import.meta.env.VITE_PLATFORM,
VITE_DEFAULT_ENDORSER_API_SERVER: import.meta.env
.VITE_DEFAULT_ENDORSER_API_SERVER,
VITE_DEFAULT_PARTNER_API_SERVER: import.meta.env
.VITE_DEFAULT_PARTNER_API_SERVER,
NODE_ENV: process.env.NODE_ENV,
activeDid: this.activeDid,
apiServer: this.apiServer,
partnerApiServer: this.partnerApiServer,
});
this.$notify({
group: "info",
type: "info",
title: "Environment State Logged",
text: "Current environment state logged to console.",
});
}
/**
* Gets the current platform based on the API server.
*/
public getCurrentPlatform(): string {
if (this.apiServer?.includes(AppString.PROD_ENDORSER_API_SERVER)) {
return "Production";
} else if (this.apiServer?.includes(AppString.TEST_ENDORSER_API_SERVER)) {
return "Test";
} else {
return "Unknown";
}
}
}
</script>