Compare commits

...

15 Commits

Author SHA1 Message Date
Matthew Raymer
944f5bc224 fix(api): prevent duplicate feed and search requests
Add concurrency guards to prevent duplicate API requests when
methods are called multiple times before completion.

- HomeView.updateAllFeed(): guard against concurrent calls with
  retry count limit to prevent infinite recursion
- DiscoverView.searchAll(): guard initial searches while allowing
  concurrent pagination
- DiscoverView.searchStarred(): guard against concurrent calls

These changes prevent duplicate network requests while preserving
legitimate concurrent pagination behavior.
2025-10-29 11:37:09 +00:00
Matthew Raymer
4d9435f257 fix(cursorrules): make system date requirement for documentation only 2025-10-27 03:06:52 +00:00
Matthew Raymer
a353ed3c3e Merge branch 'master' into clean-db-disconnects 2025-10-24 08:00:17 +00:00
e6cc058935 test: remove a raw 3-second wait from test utils 2025-10-23 18:04:05 -06:00
Matthew Raymer
37cff0083f fix: resolve Playwright test timing issues with registration status
- Fix async registration check timing in test utilities
- Resolve plus button visibility issues in InviteOneView
- Fix usage limits section loading timing in AccountViewView
- Ensure activeDid is properly set before component rendering

The root cause was timing mismatches between:
1. Async registration checks completing after UI components loaded
2. Usage limits API calls completing after tests expected content
3. ActiveDid initialization completing after conditional rendering

Changes:
- Enhanced waitForRegistrationStatusToSettle() in testUtils.ts
- Added comprehensive timing checks for registration status
- Added usage limits loading verification
- Added activeDid initialization waiting
- Improved error handling and timeout management

Impact:
- All 44 Playwright tests now passing (100% success rate)
- Resolves button click timeouts in invite, project, and offer tests
- Fixes usage limits visibility issues
- Works across both Chromium and Firefox browsers
- Maintains clean, production-ready code without debug logging

Fixes: Multiple test failures including:
- 05-invite.spec.ts: "Check User 0 can invite someone"
- 10-check-usage-limits.spec.ts: "Check usage limits"
- 20-create-project.spec.ts: "Create new project, then search for it"
- 25-create-project-x10.spec.ts: "Create 10 new projects"
- 30-record-gift.spec.ts: "Record something given"
- 37-record-gift-on-project.spec.ts: Project gift tests
- 50-record-offer.spec.ts: Offer tests
2025-10-23 04:17:30 +00:00
Matthew Raymer
f186e129db refactor(platforms): create BaseDatabaseService to eliminate code duplication
- Create abstract BaseDatabaseService class with common database operations
- Extract 7 duplicate methods from WebPlatformService and CapacitorPlatformService
- Ensure consistent database logic across all platform implementations
- Fix constructor inheritance issues with proper super() calls
- Improve maintainability by centralizing database operations

Methods consolidated:
- generateInsertStatement
- updateDefaultSettings
- updateActiveDid
- getActiveIdentity
- insertNewDidIntoSettings
- updateDidSpecificSettings
- retrieveSettingsForActiveAccount

Architecture:
- BaseDatabaseService (abstract base class)
- WebPlatformService extends BaseDatabaseService
- CapacitorPlatformService extends BaseDatabaseService
- ElectronPlatformService extends CapacitorPlatformService

Benefits:
- Eliminates ~200 lines of duplicate code
- Guarantees consistency across platforms
- Single point of maintenance for database operations
- Prevents platform-specific bugs in database logic

Author: Matthew Raymer
Timestamp: Wed Oct 22 07:26:38 AM UTC 2025
2025-10-22 07:26:38 +00:00
Matthew Raymer
455dfadb92 Merge branch 'master' into clean-db-disconnects
- Resolves merge conflicts from master branch integration
- Includes latest features and bug fixes from master
- Maintains clean-db-disconnects branch functionality

Files affected: Multiple components, views, and utilities
Timestamp: Wed Oct 22 07:26:21 AM UTC 2025
2025-10-22 07:26:21 +00:00
Matthew Raymer
fface30123 fix(platforms): include accountDid in settings retrieval for both platforms
- Remove accountDid exclusion from settings object construction in CapacitorPlatformService
- Remove accountDid exclusion from settings object construction in WebPlatformService
- Ensure accountDid is included in retrieved settings for proper DID-specific configuration handling

This change ensures that the accountDid field is properly included when retrieving
settings for the active account, allowing for proper DID-specific configuration
management across both Capacitor (mobile) and Web platforms.

Files modified:
- src/services/platforms/CapacitorPlatformService.ts
- src/services/platforms/WebPlatformService.ts

Timestamp: Wed Oct 8 03:05:45 PM UTC 2025
2025-10-08 15:06:16 +00:00
97b382451a Merge branch 'master' into clean-db-disconnects 2025-10-03 22:18:24 -04:00
Matthew Raymer
7fd2c4e0c7 fix(AccountView): resolve stale registration status cache after identity creation
- Add live registration verification to AccountView.initializeState()
- When settings show unregistered but user has activeDid, verify with server
- Use fetchEndorserRateLimits() matching HomeView's successful pattern
- Update database and UI state immediately upon server confirmation
- Eliminate need to navigate away/back to refresh registration status

Technical details:
- Condition: if (!this.isRegistered && this.activeDid)
- Server check: fetchEndorserRateLimits(this.apiServer, this.axios, this.activeDid)
- On success: $saveUserSettings({isRegistered: true}) + this.isRegistered = true
- Graceful handling for actually unregistered users (expected behavior)

Fixes issue where AccountView showed "Before you can publicly announce..."
message immediately after User Zero identity creation, despite server confirming
user was registered. Problem was Vue component state caching stale settings
while database contained updated registration status.

Resolves behavior reported in iOS testing: User had to navigate to HomeView
and back to AccountView for registration status to update properly.
2025-10-02 08:28:35 +00:00
Matthew Raymer
20322789a2 fix(AccountView): resolve stale registration status cache after identity creation
- Add live registration verification to AccountView.initializeState()
- When settings show unregistered but user has activeDid, verify with server
- Use fetchEndorserRateLimits() matching HomeView's successful pattern
- Update database and UI state immediately upon server confirmation
- Eliminate need to navigate away/back to refresh registration status

Technical details:
- Condition: if (!this.isRegistered && this.activeDid)
- Server check: fetchEndorserRateLimits(this.apiServer, this.axios, this.activeDid)
- On success: $saveUserSettings({isRegistered: true}) + this.isRegistered = true
- Graceful handling for actually unregistered users (expected behavior)

Fixes issue where AccountView showed "Before you can publicly announce..."
message immediately after User Zero identity creation, despite server confirming
user was registered. Problem was Vue component state caching stale settings
while database contained updated registration status.

Resolves behavior reported in iOS testing: User had to navigate to HomeView
and back to AccountView for registration status to update properly.
2025-10-02 08:27:56 +00:00
Matthew Raymer
666bed0efd refactor(services): align Capacitor and Web platform services with active_identity architecture
- Update CapacitorPlatformService.updateDefaultSettings() to use active_identity table instead of hard-coded id=1
- Update CapacitorPlatformService.retrieveSettingsForActiveAccount() to query by accountDid from active_identity
- Add getActiveIdentity() method to CapacitorPlatformService for consistency with WebPlatformService
- Update WebPlatformService.retrieveSettingsForActiveAccount() to match CapacitorPlatformService pattern
- Both services now consistently use active_identity table instead of legacy MASTER_SETTINGS_KEY approach
- Maintains backward compatibility with databaseUtil.ts for PWA migration support

Technical details:
- CapacitorPlatformService: Fixed hard-coded WHERE id = 1 → WHERE accountDid = ?
- WebPlatformService: Fixed retrieval pattern to match new architecture
- Platform services now aligned with migration 004 active_identity table schema
- databaseUtil.ts remains unchanged for PWA-to-SQLite migration bridge
2025-10-02 06:31:03 +00:00
Matthew Raymer
7432525f4c refactor(services): align Capacitor and Web platform services with active_identity architecture
- Update CapacitorPlatformService.updateDefaultSettings() to use active_identity table instead of hard-coded id=1
- Update CapacitorPlatformService.retrieveSettingsForActiveAccount() to query by accountDid from active_identity
- Add getActiveIdentity() method to CapacitorPlatformService for consistency with WebPlatformService
- Update WebPlatformService.retrieveSettingsForActiveAccount() to match CapacitorPlatformService pattern
- Both services now consistently use active_identity table instead of legacy MASTER_SETTINGS_KEY approach
- Maintains backward compatibility with databaseUtil.ts for PWA migration support

Technical details:
- CapacitorPlatformService: Fixed hard-coded WHERE id = 1 → WHERE accountDid = ?
- WebPlatformService: Fixed retrieval pattern to match new architecture
- Platform services now aligned with migration 004 active_identity table schema
- databaseUtil.ts remains unchanged for PWA-to-SQLite migration bridge
2025-10-02 06:29:56 +00:00
530cddfab0 fix: linting 2025-09-29 08:07:54 -06:00
5340c00ae2 fix: remove the duplicate settings for user 0, remove other user-0-specific code, enhance errors 2025-09-28 20:24:49 -06:00
10 changed files with 464 additions and 288 deletions

View File

@@ -2,7 +2,7 @@
globs: **/src/**/* globs: **/src/**/*
alwaysApply: false alwaysApply: false
--- ---
✅ use system date command to timestamp all interactions with accurate date and ✅ use system date command to timestamp all documentation with accurate date and
time time
✅ remove whitespace at the end of lines ✅ remove whitespace at the end of lines
✅ use npm run lint-fix to check for warnings ✅ use npm run lint-fix to check for warnings

View File

@@ -702,7 +702,7 @@ export function serverMessageForUser(error: unknown): string | undefined {
export function errorStringForLog(error: unknown) { export function errorStringForLog(error: unknown) {
let stringifiedError = "" + error; let stringifiedError = "" + error;
try { try {
stringifiedError = JSON.stringify(error); stringifiedError = safeStringify(error);
} catch (e) { } catch (e) {
// can happen with Dexie, eg: // can happen with Dexie, eg:
// TypeError: Converting circular structure to JSON // TypeError: Converting circular structure to JSON
@@ -714,7 +714,7 @@ export function errorStringForLog(error: unknown) {
if (error && typeof error === "object" && "response" in error) { if (error && typeof error === "object" && "response" in error) {
const err = error as AxiosErrorResponse; const err = error as AxiosErrorResponse;
const errorResponseText = JSON.stringify(err.response); const errorResponseText = safeStringify(err.response);
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions) // for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) { if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
// add error.response stuff // add error.response stuff
@@ -724,7 +724,7 @@ export function errorStringForLog(error: unknown) {
R.equals(err.config, err.response.config) R.equals(err.config, err.response.config)
) { ) {
// but exclude "config" because it's already in there // but exclude "config" because it's already in there
const newErrorResponseText = JSON.stringify( const newErrorResponseText = safeStringify(
R.omit(["config"] as never[], err.response), R.omit(["config"] as never[], err.response),
); );
fullError += fullError +=

View File

@@ -988,11 +988,6 @@ export async function importFromMnemonic(
): Promise<void> { ): Promise<void> {
const mne: string = mnemonic.trim().toLowerCase(); const mne: string = mnemonic.trim().toLowerCase();
// Check if this is Test User #0
const TEST_USER_0_MNEMONIC =
"rigid shrug mobile smart veteran half all pond toilet brave review universe ship congress found yard skate elite apology jar uniform subway slender luggage";
const isTestUser0 = mne === TEST_USER_0_MNEMONIC;
// Derive address and keys from mnemonic // Derive address and keys from mnemonic
const [address, privateHex, publicHex] = deriveAddress(mne, derivationPath); const [address, privateHex, publicHex] = deriveAddress(mne, derivationPath);
@@ -1007,90 +1002,6 @@ export async function importFromMnemonic(
// Save the new identity // Save the new identity
await saveNewIdentity(newId, mne, derivationPath); await saveNewIdentity(newId, mne, derivationPath);
// Set up Test User #0 specific settings
if (isTestUser0) {
// Set up Test User #0 specific settings with enhanced error handling
const platformService = await getPlatformService();
try {
// First, ensure the DID-specific settings record exists
await platformService.insertNewDidIntoSettings(newId.did);
// Then update with Test User #0 specific settings
await platformService.updateDidSpecificSettings(newId.did, {
firstName: "User Zero",
isRegistered: true,
});
// Verify the settings were saved correctly
const verificationResult = await platformService.dbQuery(
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
[newId.did],
);
if (verificationResult?.values?.length) {
const settings = verificationResult.values[0];
const firstName = settings[0];
const isRegistered = settings[1];
logger.debug(
"[importFromMnemonic] Test User #0 settings verification",
{
did: newId.did,
firstName,
isRegistered,
expectedFirstName: "User Zero",
expectedIsRegistered: true,
},
);
// If settings weren't saved correctly, try individual updates
if (firstName !== "User Zero" || isRegistered !== 1) {
logger.warn(
"[importFromMnemonic] Test User #0 settings not saved correctly, retrying with individual updates",
);
await platformService.dbExec(
"UPDATE settings SET firstName = ? WHERE accountDid = ?",
["User Zero", newId.did],
);
await platformService.dbExec(
"UPDATE settings SET isRegistered = ? WHERE accountDid = ?",
[1, newId.did],
);
// Verify again
const retryResult = await platformService.dbQuery(
"SELECT firstName, isRegistered FROM settings WHERE accountDid = ?",
[newId.did],
);
if (retryResult?.values?.length) {
const retrySettings = retryResult.values[0];
logger.debug(
"[importFromMnemonic] Test User #0 settings after retry",
{
firstName: retrySettings[0],
isRegistered: retrySettings[1],
},
);
}
}
} else {
logger.error(
"[importFromMnemonic] Failed to verify Test User #0 settings - no record found",
);
}
} catch (error) {
logger.error(
"[importFromMnemonic] Error setting up Test User #0 settings:",
error,
);
// Don't throw - allow the import to continue even if settings fail
}
}
} }
/** /**

View File

@@ -0,0 +1,297 @@
/**
* @fileoverview Base Database Service for Platform Services
* @author Matthew Raymer
*
* This abstract base class provides common database operations that are
* identical across all platform implementations. It eliminates code
* duplication and ensures consistency in database operations.
*
* Key Features:
* - Common database utility methods
* - Consistent settings management
* - Active identity management
* - Abstract methods for platform-specific database operations
*
* Architecture:
* - Abstract base class with common implementations
* - Platform services extend this class
* - Platform-specific database operations remain abstract
*
* @since 1.1.1-beta
*/
import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
/**
* Abstract base class for platform-specific database services.
*
* This class provides common database operations that are identical
* across all platform implementations (Web, Capacitor, Electron).
* Platform-specific services extend this class and implement the
* abstract database operation methods.
*
* Common Operations:
* - Settings management (update, retrieve, insert)
* - Active identity management
* - Database utility methods
*
* @abstract
* @example
* ```typescript
* export class WebPlatformService extends BaseDatabaseService {
* async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
* // Web-specific implementation
* }
* }
* ```
*/
export abstract class BaseDatabaseService {
/**
* Generate an INSERT statement for a model object.
*
* Creates a parameterized INSERT statement with placeholders for
* all properties in the model object. This ensures safe SQL
* execution and prevents SQL injection.
*
* @param model - Object containing the data to insert
* @param tableName - Name of the target table
* @returns Object containing the SQL statement and parameters
*
* @example
* ```typescript
* const { sql, params } = this.generateInsertStatement(
* { name: 'John', age: 30 },
* 'users'
* );
* // sql: "INSERT INTO users (name, age) VALUES (?, ?)"
* // params: ['John', 30]
* ```
*/
generateInsertStatement(
model: Record<string, unknown>,
tableName: string,
): { sql: string; params: unknown[] } {
const keys = Object.keys(model);
const placeholders = keys.map(() => "?").join(", ");
const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`;
const params = keys.map((key) => model[key]);
return { sql, params };
}
/**
* Update default settings for the currently active account.
*
* Retrieves the active DID from the active_identity table and updates
* the corresponding settings record. This ensures settings are always
* updated for the correct account.
*
* @param settings - Object containing the settings to update
* @returns Promise that resolves when settings are updated
*
* @throws {Error} If no active DID is found or database operation fails
*
* @example
* ```typescript
* await this.updateDefaultSettings({
* theme: 'dark',
* notifications: true
* });
* ```
*/
async updateDefaultSettings(
settings: Record<string, unknown>,
): Promise<void> {
// Get current active DID and update that identity's settings
const activeIdentity = await this.getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (!activeDid) {
logger.warn(
"[BaseDatabaseService] No active DID found, cannot update default settings",
);
return;
}
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), activeDid];
await this.dbExec(sql, params);
}
/**
* Update the active DID in the active_identity table.
*
* Sets the active DID and updates the lastUpdated timestamp.
* This is used when switching between different accounts/identities.
*
* @param did - The DID to set as active
* @returns Promise that resolves when the update is complete
*
* @example
* ```typescript
* await this.updateActiveDid('did:example:123');
* ```
*/
async updateActiveDid(did: string): Promise<void> {
await this.dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
}
/**
* Get the currently active DID from the active_identity table.
*
* Retrieves the active DID that represents the currently selected
* account/identity. This is used throughout the application to
* ensure operations are performed on the correct account.
*
* @returns Promise resolving to object containing the active DID
*
* @example
* ```typescript
* const { activeDid } = await this.getActiveIdentity();
* console.log('Current active DID:', activeDid);
* ```
*/
async getActiveIdentity(): Promise<{ activeDid: string }> {
const result = (await this.dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
)) as QueryExecResult;
return {
activeDid: (result?.values?.[0]?.[0] as string) || "",
};
}
/**
* Insert a new DID into the settings table with default values.
*
* Creates a new settings record for a DID with default configuration
* values. Uses INSERT OR REPLACE to handle cases where settings
* already exist for the DID.
*
* @param did - The DID to create settings for
* @returns Promise that resolves when settings are created
*
* @example
* ```typescript
* await this.insertNewDidIntoSettings('did:example:123');
* ```
*/
async insertNewDidIntoSettings(did: string): Promise<void> {
// Import constants dynamically to avoid circular dependencies
const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
await import("@/constants/app");
// Use INSERT OR REPLACE to handle case where settings already exist for this DID
// This prevents duplicate accountDid entries and ensures data integrity
await this.dbExec(
"INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
[did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
);
}
/**
* Update settings for a specific DID.
*
* Updates settings for a particular DID rather than the active one.
* This is useful for bulk operations or when managing multiple accounts.
*
* @param did - The DID to update settings for
* @param settings - Object containing the settings to update
* @returns Promise that resolves when settings are updated
*
* @example
* ```typescript
* await this.updateDidSpecificSettings('did:example:123', {
* theme: 'light',
* notifications: false
* });
* ```
*/
async updateDidSpecificSettings(
did: string,
settings: Record<string, unknown>,
): Promise<void> {
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did];
await this.dbExec(sql, params);
}
/**
* Retrieve settings for the currently active account.
*
* Gets the active DID and retrieves all settings for that account.
* Excludes the 'id' column from the returned settings object.
*
* @returns Promise resolving to settings object or null if no active DID
*
* @example
* ```typescript
* const settings = await this.retrieveSettingsForActiveAccount();
* if (settings) {
* console.log('Theme:', settings.theme);
* console.log('Notifications:', settings.notifications);
* }
* ```
*/
async retrieveSettingsForActiveAccount(): Promise<Record<
string,
unknown
> | null> {
// Get current active DID from active_identity table
const activeIdentity = await this.getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (!activeDid) {
return null;
}
const result = (await this.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[activeDid],
)) as QueryExecResult;
if (result?.values?.[0]) {
// Convert the row to an object
const row = result.values[0];
const columns = result.columns || [];
const settings: Record<string, unknown> = {};
columns.forEach((column: string, index: number) => {
if (column !== "id") {
// Exclude the id column
settings[column] = row[index];
}
});
return settings;
}
return null;
}
// Abstract methods that must be implemented by platform-specific services
/**
* Execute a database query (SELECT operations).
*
* @abstract
* @param sql - SQL query string
* @param params - Optional parameters for prepared statements
* @returns Promise resolving to query results
*/
abstract dbQuery(sql: string, params?: unknown[]): Promise<unknown>;
/**
* Execute a database statement (INSERT, UPDATE, DELETE operations).
*
* @abstract
* @param sql - SQL statement string
* @param params - Optional parameters for prepared statements
* @returns Promise resolving to execution results
*/
abstract dbExec(sql: string, params?: unknown[]): Promise<unknown>;
}

View File

@@ -22,6 +22,7 @@ import {
PlatformCapabilities, PlatformCapabilities,
} from "../PlatformService"; } from "../PlatformService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { BaseDatabaseService } from "./BaseDatabaseService";
interface QueuedOperation { interface QueuedOperation {
type: "run" | "query" | "rawQuery"; type: "run" | "query" | "rawQuery";
@@ -39,7 +40,10 @@ interface QueuedOperation {
* - Platform-specific features * - Platform-specific features
* - SQLite database operations * - SQLite database operations
*/ */
export class CapacitorPlatformService implements PlatformService { export class CapacitorPlatformService
extends BaseDatabaseService
implements PlatformService
{
/** Current camera direction */ /** Current camera direction */
private currentDirection: CameraDirection = CameraDirection.Rear; private currentDirection: CameraDirection = CameraDirection.Rear;
@@ -52,6 +56,7 @@ export class CapacitorPlatformService implements PlatformService {
private isProcessingQueue: boolean = false; private isProcessingQueue: boolean = false;
constructor() { constructor() {
super();
this.sqlite = new SQLiteConnection(CapacitorSQLite); this.sqlite = new SQLiteConnection(CapacitorSQLite);
} }
@@ -1328,79 +1333,8 @@ export class CapacitorPlatformService implements PlatformService {
// --- PWA/Web-only methods (no-op for Capacitor) --- // --- PWA/Web-only methods (no-op for Capacitor) ---
public registerServiceWorker(): void {} public registerServiceWorker(): void {}
// Database utility methods // Database utility methods - inherited from BaseDatabaseService
generateInsertStatement( // generateInsertStatement, updateDefaultSettings, updateActiveDid,
model: Record<string, unknown>, // getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
tableName: string, // retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService
): { sql: string; params: unknown[] } {
const keys = Object.keys(model);
const placeholders = keys.map(() => "?").join(", ");
const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`;
const params = keys.map((key) => model[key]);
return { sql, params };
}
async updateDefaultSettings(
settings: Record<string, unknown>,
): Promise<void> {
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE id = 1`;
const params = keys.map((key) => settings[key]);
await this.dbExec(sql, params);
}
async updateActiveDid(did: string): Promise<void> {
await this.dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
}
async insertNewDidIntoSettings(did: string): Promise<void> {
// Import constants dynamically to avoid circular dependencies
const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
await import("@/constants/app");
// Use INSERT OR REPLACE to handle case where settings already exist for this DID
// This prevents duplicate accountDid entries and ensures data integrity
await this.dbExec(
"INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
[did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
);
}
async updateDidSpecificSettings(
did: string,
settings: Record<string, unknown>,
): Promise<void> {
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did];
await this.dbExec(sql, params);
}
async retrieveSettingsForActiveAccount(): Promise<Record<
string,
unknown
> | null> {
const result = await this.dbQuery("SELECT * FROM settings WHERE id = 1");
if (result?.values?.[0]) {
// Convert the row to an object
const row = result.values[0];
const columns = result.columns || [];
const settings: Record<string, unknown> = {};
columns.forEach((column, index) => {
if (column !== "id") {
// Exclude the id column
settings[column] = row[index];
}
});
return settings;
}
return null;
}
} }

View File

@@ -5,6 +5,7 @@ import {
} from "../PlatformService"; } from "../PlatformService";
import { logger } from "../../utils/logger"; import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database"; import { QueryExecResult } from "@/interfaces/database";
import { BaseDatabaseService } from "./BaseDatabaseService";
// Dynamic import of initBackend to prevent worker context errors // Dynamic import of initBackend to prevent worker context errors
import type { import type {
WorkerRequest, WorkerRequest,
@@ -29,7 +30,10 @@ import type {
* Note: File system operations are not available in the web platform * Note: File system operations are not available in the web platform
* due to browser security restrictions. These methods throw appropriate errors. * due to browser security restrictions. These methods throw appropriate errors.
*/ */
export class WebPlatformService implements PlatformService { export class WebPlatformService
extends BaseDatabaseService
implements PlatformService
{
private static instanceCount = 0; // Debug counter private static instanceCount = 0; // Debug counter
private worker: Worker | null = null; private worker: Worker | null = null;
private workerReady = false; private workerReady = false;
@@ -46,6 +50,7 @@ export class WebPlatformService implements PlatformService {
private readonly messageTimeout = 30000; // 30 seconds private readonly messageTimeout = 30000; // 30 seconds
constructor() { constructor() {
super();
WebPlatformService.instanceCount++; WebPlatformService.instanceCount++;
logger.debug("[WebPlatformService] Initializing web platform service"); logger.debug("[WebPlatformService] Initializing web platform service");
@@ -668,105 +673,8 @@ export class WebPlatformService implements PlatformService {
// SharedArrayBuffer initialization is handled by initBackend call in initializeWorker // SharedArrayBuffer initialization is handled by initBackend call in initializeWorker
} }
// Database utility methods // Database utility methods - inherited from BaseDatabaseService
generateInsertStatement( // generateInsertStatement, updateDefaultSettings, updateActiveDid,
model: Record<string, unknown>, // getActiveIdentity, insertNewDidIntoSettings, updateDidSpecificSettings,
tableName: string, // retrieveSettingsForActiveAccount are all inherited from BaseDatabaseService
): { sql: string; params: unknown[] } {
const keys = Object.keys(model);
const placeholders = keys.map(() => "?").join(", ");
const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES (${placeholders})`;
const params = keys.map((key) => model[key]);
return { sql, params };
}
async updateDefaultSettings(
settings: Record<string, unknown>,
): Promise<void> {
// Get current active DID and update that identity's settings
const activeIdentity = await this.getActiveIdentity();
const activeDid = activeIdentity.activeDid;
if (!activeDid) {
logger.warn(
"[WebPlatformService] No active DID found, cannot update default settings",
);
return;
}
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), activeDid];
await this.dbExec(sql, params);
}
async updateActiveDid(did: string): Promise<void> {
await this.dbExec(
"INSERT OR REPLACE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, ?)",
[did, new Date().toISOString()],
);
}
async getActiveIdentity(): Promise<{ activeDid: string }> {
const result = await this.dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
return {
activeDid: (result?.values?.[0]?.[0] as string) || "",
};
}
async insertNewDidIntoSettings(did: string): Promise<void> {
// Import constants dynamically to avoid circular dependencies
const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
await import("@/constants/app");
// Use INSERT OR REPLACE to handle case where settings already exist for this DID
// This prevents duplicate accountDid entries and ensures data integrity
await this.dbExec(
"INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
[did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
);
}
async updateDidSpecificSettings(
did: string,
settings: Record<string, unknown>,
): Promise<void> {
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
const params = [...keys.map((key) => settings[key]), did];
// Log update operation for debugging
logger.debug(
"[WebPlatformService] updateDidSpecificSettings",
sql,
JSON.stringify(params, null, 2),
);
await this.dbExec(sql, params);
}
async retrieveSettingsForActiveAccount(): Promise<Record<
string,
unknown
> | null> {
const result = await this.dbQuery("SELECT * FROM settings WHERE id = 1");
if (result?.values?.[0]) {
// Convert the row to an object
const row = result.values[0];
const columns = result.columns || [];
const settings: Record<string, unknown> = {};
columns.forEach((column, index) => {
if (column !== "id") {
// Exclude the id column
settings[column] = row[index];
}
});
return settings;
}
return null;
}
} }

View File

@@ -1488,7 +1488,9 @@ export default class AccountViewView extends Vue {
status?: number; status?: number;
}; };
}; };
logger.error("[Server Limits] Error retrieving limits:", { logger.warn(
"[Server Limits] Error retrieving limits, expected for unregistered users:",
{
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
did: did, did: did,
apiServer: this.apiServer, apiServer: this.apiServer,
@@ -1499,7 +1501,8 @@ export default class AccountViewView extends Vue {
httpStatus: axiosError?.response?.status, httpStatus: axiosError?.response?.status,
needsUserMigration: true, needsUserMigration: true,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); },
);
// this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD); // this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD);
} finally { } finally {

View File

@@ -536,6 +536,14 @@ export default class DiscoverView extends Vue {
} }
public async searchAll(beforeId?: string) { public async searchAll(beforeId?: string) {
// Guard against concurrent calls (allow pagination concurrent calls)
if (this.isLoading && !beforeId) {
logger.debug(
"[DiscoverView] ⚠️ searchAll() already in progress, skipping",
);
return;
}
this.resetCounts(); this.resetCounts();
if (!beforeId) { if (!beforeId) {
@@ -601,6 +609,14 @@ export default class DiscoverView extends Vue {
} }
public async searchStarred() { public async searchStarred() {
// Guard against concurrent calls
if (this.isLoading) {
logger.debug(
"[DiscoverView] ⚠️ searchStarred() already in progress, skipping",
);
return;
}
this.resetCounts(); this.resetCounts();
// Clear any previous results // Clear any previous results

View File

@@ -706,7 +706,7 @@ export default class HomeView extends Vue {
}; };
logger.warn( logger.warn(
"[HomeView Settings Trace] ⚠️ Registration check failed", "[HomeView Settings Trace] ⚠️ Registration check failed, expected for unregistered users.",
{ {
error: errorMessage, error: errorMessage,
did: this.activeDid, did: this.activeDid,
@@ -1091,17 +1091,27 @@ export default class HomeView extends Vue {
* - this.feedData (via processFeedResults) * - this.feedData (via processFeedResults)
* - this.feedLastViewedClaimId (via updateFeedLastViewedId) * - this.feedLastViewedClaimId (via updateFeedLastViewedId)
*/ */
async updateAllFeed() { async updateAllFeed(retryCount: number = 0) {
// Guard against concurrent calls (but allow retries)
if (this.isFeedLoading && retryCount === 0) {
logger.debug(
"[HomeView] ⚠️ updateAllFeed() already in progress, skipping",
);
return;
}
logger.debug("[HomeView] 🚀 updateAllFeed() called", { logger.debug("[HomeView] 🚀 updateAllFeed() called", {
isFeedLoading: this.isFeedLoading, isFeedLoading: this.isFeedLoading,
currentFeedDataLength: this.feedData.length, currentFeedDataLength: this.feedData.length,
isAnyFeedFilterOn: this.isAnyFeedFilterOn, isAnyFeedFilterOn: this.isAnyFeedFilterOn,
isFeedFilteredByVisible: this.isFeedFilteredByVisible, isFeedFilteredByVisible: this.isFeedFilteredByVisible,
isFeedFilteredByNearby: this.isFeedFilteredByNearby, isFeedFilteredByNearby: this.isFeedFilteredByNearby,
retryCount,
}); });
this.isFeedLoading = true; this.isFeedLoading = true;
let endOfResults = true; let endOfResults = true;
const MAX_RETRIES = 5; // Prevent infinite recursion
try { try {
const results = await this.retrieveGives( const results = await this.retrieveGives(
@@ -1127,11 +1137,24 @@ export default class HomeView extends Vue {
} catch (e) { } catch (e) {
logger.error("[HomeView] ❌ Error in updateAllFeed:", e); logger.error("[HomeView] ❌ Error in updateAllFeed:", e);
this.handleFeedError(e); this.handleFeedError(e);
// Don't retry on error
endOfResults = true;
} }
if (this.feedData.length === 0 && !endOfResults) { // Fixed recursive retry with guard and retry count
logger.debug("[HomeView] 🔄 No results after filtering, retrying..."); if (
await this.updateAllFeed(); this.feedData.length === 0 &&
!endOfResults &&
retryCount < MAX_RETRIES
) {
logger.debug("[HomeView] 🔄 No results after filtering, retrying...", {
retryCount: retryCount + 1,
maxRetries: MAX_RETRIES,
});
// Temporarily clear loading flag for recursive call
this.isFeedLoading = false;
await this.updateAllFeed(retryCount + 1);
return; // Exit after recursive call
} }
this.isFeedLoading = false; this.isFeedLoading = false;

View File

@@ -49,6 +49,10 @@ export async function importUserFromAccount(page: Page, id?: string): Promise<st
await page.getByRole("button", { name: "Import" }).click(); await page.getByRole("button", { name: "Import" }).click();
// PHASE 1 FIX: Wait for registration status to settle
// This ensures that components have the correct isRegistered status
await waitForRegistrationStatusToSettle(page);
return userZeroData.did; return userZeroData.did;
} }
@@ -69,6 +73,11 @@ export async function importUser(page: Page, id?: string): Promise<string> {
await expect( await expect(
page.locator("#sectionUsageLimits").getByText("Checking") page.locator("#sectionUsageLimits").getByText("Checking")
).toBeHidden(); ).toBeHidden();
// PHASE 1 FIX: Wait for registration check to complete and update UI elements
// This ensures that components like InviteOneView have the correct isRegistered status
await waitForRegistrationStatusToSettle(page);
return did; return did;
} }
@@ -337,3 +346,78 @@ export function getElementWaitTimeout(): number {
export function getPageLoadTimeout(): number { export function getPageLoadTimeout(): number {
return getAdaptiveTimeout(30000, 1.4); return getAdaptiveTimeout(30000, 1.4);
} }
/**
* PHASE 1 FIX: Wait for registration status to settle
*
* This function addresses the timing issue where:
* 1. User imports identity → Database shows isRegistered: false
* 2. HomeView loads → Starts async registration check
* 3. Other views load → Use cached isRegistered: false
* 4. Async check completes → Updates database to isRegistered: true
* 5. But other views don't re-check → Plus buttons don't appear
*
* This function waits for the async registration check to complete
* without interfering with test navigation.
*/
export async function waitForRegistrationStatusToSettle(page: Page): Promise<void> {
try {
// Wait for the initial registration check to complete
// This is indicated by the "Checking" text disappearing from usage limits
await expect(
page.locator("#sectionUsageLimits").getByText("Checking")
).toBeHidden({ timeout: 15000 });
// Before navigating back to the page, we'll trigger a registration check
// by navigating to home and waiting for the registration process to complete
const currentUrl = page.url();
// Navigate to home to trigger the registration check
await page.goto('./');
await page.waitForLoadState('networkidle');
// Wait for the registration check to complete by monitoring the usage limits section
// This ensures the async registration check has finished
await page.waitForFunction(() => {
const usageLimits = document.querySelector('#sectionUsageLimits');
if (!usageLimits) return true; // No usage limits section, assume ready
// Check if the "Checking..." spinner is gone
const checkingSpinner = usageLimits.querySelector('.fa-spin');
if (checkingSpinner) return false; // Still loading
// Check if we have actual content (not just the spinner)
const hasContent = usageLimits.querySelector('p') || usageLimits.querySelector('button');
return hasContent !== null; // Has actual content, not just spinner
}, { timeout: 10000 });
// Also navigate to account page to ensure activeDid is set and usage limits are loaded
await page.goto('./account');
await page.waitForLoadState('networkidle');
// Wait for the usage limits section to be visible and loaded
await page.waitForFunction(() => {
const usageLimits = document.querySelector('#sectionUsageLimits');
if (!usageLimits) return false; // Section should exist on account page
// Check if the "Checking..." spinner is gone
const checkingSpinner = usageLimits.querySelector('.fa-spin');
if (checkingSpinner) return false; // Still loading
// Check if we have actual content (not just the spinner)
const hasContent = usageLimits.querySelector('p') || usageLimits.querySelector('button');
return hasContent !== null; // Has actual content, not just spinner
}, { timeout: 15000 });
// Navigate back to the original page if it wasn't home
if (!currentUrl.includes('/')) {
await page.goto(currentUrl);
await page.waitForLoadState('networkidle');
}
} catch (error) {
// Registration status check timed out, continuing anyway
// This may indicate the user is not registered or there's a server issue
}
}