From aed16ebe94408a08d1abac8995560790406c740f Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 4 Aug 2025 03:52:21 +0000 Subject: [PATCH 1/2] Remove PROD_SHARE_DOMAIN constant and unify domain configuration - Remove hard-coded PROD_SHARE_DOMAIN from src/constants/app.ts - Update all sharing functionality to use environment-specific APP_SERVER - Rewrite domain configuration documentation to reflect unified approach - Simplify domain management with single APP_SERVER constant - Update README.md examples to use APP_SERVER for all URL generation This change eliminates the complexity of separate constants for different URL types and provides consistent environment-specific configuration for all functionality including sharing. --- README.md | 8 +- docs/development/domain-configuration.md | 140 +++++++++++------------ src/constants/app.ts | 3 - 3 files changed, 69 insertions(+), 82 deletions(-) diff --git a/README.md b/README.md index 12a1ce02..2650ae23 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,11 @@ appearing in shared links during development. - ✅ **Type-Safe Configuration**: Full TypeScript support ### Quick Reference + ```typescript -// For sharing functionality (always production) -import { PROD_SHARE_DOMAIN } from "@/constants/app"; -const shareLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`; +// For sharing functionality (environment-specific) +import { APP_SERVER } from "@/constants/app"; +const shareLink = `${APP_SERVER}/deep-link/claim/123`; // For internal operations (environment-specific) import { APP_SERVER } from "@/constants/app"; @@ -124,6 +125,7 @@ const apiUrl = `${APP_SERVER}/api/claim/123`; ``` ### Documentation + - [Domain Configuration System](docs/domain-configuration.md) - Complete guide - [Constants and Configuration](src/constants/app.ts) - Core constants diff --git a/docs/development/domain-configuration.md b/docs/development/domain-configuration.md index eaaabb19..927829a1 100644 --- a/docs/development/domain-configuration.md +++ b/docs/development/domain-configuration.md @@ -2,33 +2,30 @@ **Author**: Matthew Raymer **Date**: 2025-01-27 -**Status**: ✅ **COMPLETE** - Domain configuration system implemented +**Status**: ✅ **UPDATED** - Simplified to use APP_SERVER for all functionality ## Overview TimeSafari uses a centralized domain configuration system to ensure consistent -URL generation across all environments. This system prevents localhost URLs from -appearing in shared links during development and provides a single point of -control for domain changes. +URL generation across all environments. This system provides a single point of +control for domain changes and uses environment-specific configuration for all +functionality including sharing. ## Problem Solved -### Issue: Localhost URLs in Shared Links +### Issue: Inconsistent Domain Usage -Previously, copy link buttons and deep link generation used the environment- -specific `APP_SERVER` constant, which resulted in: +Previously, the system used separate constants for different types of URLs: -- **Development**: `http://localhost:8080/deep-link/claim/123` -- **Test**: `https://test.timesafari.app/deep-link/claim/123` -- **Production**: `https://timesafari.app/deep-link/claim/123` +- **Internal Operations**: Used `APP_SERVER` (environment-specific) +- **Sharing**: Used separate constants (removed) -This caused problems when users in development mode shared links, as the -localhost URLs wouldn't work for other users. +This created complexity and confusion about when to use which constant. -### Solution: Production Domain for Sharing +### Solution: Unified Domain Configuration -All sharing functionality now uses the `PROD_SHARE_DOMAIN` constant, which -always points to the production domain regardless of the current environment. +All functionality now uses the `APP_SERVER` constant, which provides +environment-specific URLs that can be configured per environment. ## Implementation @@ -43,27 +40,28 @@ export enum AppString { // ... other constants ... } -// Production domain for sharing links (always use production URL for sharing) -export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER; +// Environment-specific server URL for all functionality +export const APP_SERVER = + import.meta.env.VITE_APP_SERVER || "https://timesafari.app"; ``` ### Usage Pattern -All components that generate shareable links follow this pattern: +All components that generate URLs follow this pattern: ```typescript -import { PROD_SHARE_DOMAIN } from "@/constants/app"; +import { APP_SERVER } from "@/constants/app"; // In component class -PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN; +APP_SERVER = APP_SERVER; // In methods -const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/${claimId}`; +const deepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; ``` ### Components Updated -The following components and services were updated to use `PROD_SHARE_DOMAIN`: +The following components and services use `APP_SERVER`: #### Views - `ClaimView.vue` - Claim and certificate links @@ -82,17 +80,28 @@ The following components and services were updated to use `PROD_SHARE_DOMAIN`: ## Configuration Management -### Changing the Production Domain +### Environment-Specific Configuration + +The system uses environment variables to configure domains: -To change the production domain for all sharing functionality: +```bash +# Development +VITE_APP_SERVER=http://localhost:8080 -1. **Update the constant** in `src/constants/app.ts`: - ```typescript - export enum AppString { - // ... other constants ... - PROD_PUSH_SERVER = "https://your-new-domain.com", - // ... other constants ... - } +# Test +VITE_APP_SERVER=https://test.timesafari.app + +# Production +VITE_APP_SERVER=https://timesafari.app +``` + +### Changing the Domain + +To change the domain for all functionality: + +1. **Update environment variables** for the target environment: + ```bash + VITE_APP_SERVER=https://your-new-domain.com ``` 2. **Rebuild the application** for all platforms: @@ -102,46 +111,32 @@ To change the production domain for all sharing functionality: npm run build:electron ``` -### Environment-Specific Configuration - -The system maintains environment-specific configuration for internal operations -while using production domains for sharing: - -```typescript -// Internal operations use environment-specific URLs -export const APP_SERVER = - import.meta.env.VITE_APP_SERVER || "https://timesafari.app"; +## Benefits -// Sharing always uses production URLs -export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER; -``` +### ✅ Simplified Configuration -## Benefits +- Single constant for all URL generation +- No confusion about which constant to use +- Consistent behavior across all functionality -### ✅ Consistent User Experience +### ✅ Environment Flexibility -- All shared links work for all users regardless of environment -- No more broken localhost links in development -- Consistent behavior across all platforms +- Easy to configure different domains per environment +- Support for development, test, and production environments +- Environment-specific sharing URLs when needed ### ✅ Maintainability -- Single source of truth for production domain +- Single source of truth for domain configuration - Easy to change domain across entire application -- Clear separation between internal and sharing URLs +- Clear pattern for implementing new URL functionality ### ✅ Developer Experience -- No need to remember which environment URLs work for sharing -- Clear pattern for implementing new sharing functionality +- Simple, consistent pattern for URL generation +- Clear documentation and examples - Type-safe configuration with TypeScript -### ✅ Security - -- No accidental exposure of internal development URLs -- Controlled domain configuration -- Clear audit trail for domain changes - ## Testing ### Manual Testing @@ -150,7 +145,7 @@ export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER; ```bash npm run dev # Navigate to any page with copy link buttons - # Verify links use production domain, not localhost + # Verify links use configured domain ``` 2. **Production Build**: @@ -164,27 +159,19 @@ export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER; The implementation includes comprehensive linting to ensure: -- All components properly import `PROD_SHARE_DOMAIN` -- No hardcoded URLs in sharing functionality +- All components properly import `APP_SERVER` +- No hardcoded URLs in functionality - Consistent usage patterns across the codebase -## Migration Notes +## Implementation Pattern -### Before Implementation +### Current Approach ```typescript -// ❌ Hardcoded URLs -const deepLink = "https://timesafari.app/deep-link/claim/123"; - -// ❌ Environment-specific URLs -const deepLink = `${APP_SERVER}/deep-link/claim/123`; -``` - -### After Implementation - -```typescript -// ✅ Configurable production URLs -const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`; +// ✅ Single constant for all functionality +import { APP_SERVER } from "@/constants/app"; +const shareLink = `${APP_SERVER}/deep-link/claim/123`; +const apiUrl = `${APP_SERVER}/api/claim/123`; ``` ## Future Enhancements @@ -208,6 +195,7 @@ const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`; ``` 3. **Platform-Specific Domains**: + ```typescript export const getPlatformShareDomain = () => { const platform = process.env.VITE_PLATFORM; @@ -229,5 +217,5 @@ const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/123`; --- **Last Updated**: 2025-01-27 -**Version**: 1.0 +**Version**: 2.0 **Maintainer**: Matthew Raymer \ No newline at end of file diff --git a/src/constants/app.ts b/src/constants/app.ts index 0d172dcc..aa64ea32 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -47,9 +47,6 @@ export const DEFAULT_PARTNER_API_SERVER = export const DEFAULT_PUSH_SERVER = import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER; -// Production domain for sharing links (always use production URL for sharing) -export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER; - export const IMAGE_TYPE_PROFILE = "profile"; export const PASSKEYS_ENABLED = From 0bd0e7c332f28e1a221d721a8d5d551d0580ebb4 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 4 Aug 2025 04:41:54 +0000 Subject: [PATCH 2/2] Fix contact methods JSON string/array duality in PlatformServiceMixin - Add ContactMaybeWithJsonStrings type usage for internal database operations - Implement $normalizeContacts() method to handle both JSON string and array formats - Update $contacts(), $getContact(), and $getAllContacts() to use normalization - Fix $updateContact() to properly convert contactMethods arrays to JSON strings - Add validation to filter out malformed contact method objects - Update ContactEditView to handle malformed data gracefully Resolves issue where contactMethods could be stored as JSON strings in database but expected as arrays in components, causing "Cannot create property 'label' on number '0'" errors. --- src/db/tables/contacts.ts | 11 +++ src/utils/PlatformServiceMixin.ts | 118 +++++++++++++++++++++++------- src/views/ContactEditView.vue | 16 +++- 3 files changed, 117 insertions(+), 28 deletions(-) diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index 3dfa6e1b..cfb88798 100644 --- a/src/db/tables/contacts.ts +++ b/src/db/tables/contacts.ts @@ -32,6 +32,17 @@ export type ContactWithJsonStrings = Omit & { contactMethods?: string; }; +/** + * This is for those cases (eg. with a DB) where field values may be all primitives or may be JSON values. + * See src/db/databaseUtil.ts parseJsonField for more details. + * + * This is so that we can reuse most of the type and don't have to maintain another copy. + * Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2 + */ +export type ContactMaybeWithJsonStrings = Omit & { + contactMethods?: string | Array; +}; + export const ContactSchema = { contacts: "&did, name", // no need to key by other things }; diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 6baa8cc6..47a943c0 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -50,7 +50,7 @@ import { type SettingsWithJsonStrings, } from "@/db/tables/settings"; import { logger } from "@/utils/logger"; -import { Contact } from "@/db/tables/contacts"; +import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts"; import { Account } from "@/db/tables/accounts"; import { Temp } from "@/db/tables/temp"; import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database"; @@ -642,15 +642,81 @@ export const PlatformServiceMixin = { // CACHED SPECIALIZED SHORTCUTS (massive performance boost) // ================================================= + /** + * Normalize contact data by parsing JSON strings into proper objects + * Handles the contactMethods field which can be either a JSON string or an array + * @param rawContacts Raw contact data from database + * @returns Normalized Contact[] array + */ + $normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[] { + return rawContacts.map((contact) => { + // Create a new contact object with proper typing + const normalizedContact: Contact = { + did: contact.did, + iViewContent: contact.iViewContent, + name: contact.name, + nextPubKeyHashB64: contact.nextPubKeyHashB64, + notes: contact.notes, + profileImageUrl: contact.profileImageUrl, + publicKeyBase64: contact.publicKeyBase64, + seesMe: contact.seesMe, + registered: contact.registered, + }; + + // Handle contactMethods field which can be a JSON string or an array + if (contact.contactMethods !== undefined) { + if (typeof contact.contactMethods === "string") { + // Parse JSON string into array + normalizedContact.contactMethods = this._parseJsonField( + contact.contactMethods, + [], + ); + } else if (Array.isArray(contact.contactMethods)) { + // Validate that each item in the array is a proper ContactMethod object + normalizedContact.contactMethods = contact.contactMethods.filter( + (method) => { + const isValid = + method && + typeof method === "object" && + typeof method.label === "string" && + typeof method.type === "string" && + typeof method.value === "string"; + + if (!isValid && method !== undefined) { + console.warn( + "[ContactNormalization] Invalid contact method:", + method, + ); + } + + return isValid; + }, + ); + } else { + // Invalid data, use empty array + normalizedContact.contactMethods = []; + } + } else { + // No contactMethods, use empty array + normalizedContact.contactMethods = []; + } + + return normalizedContact; + }); + }, + /** * Load all contacts (always fresh) - $contacts() * Always fetches fresh data from database for consistency - * @returns Promise Array of contact objects + * Handles JSON string/object duality for contactMethods field + * @returns Promise Array of normalized contact objects */ async $contacts(): Promise { - return (await this.$query( + const rawContacts = (await this.$query( "SELECT * FROM contacts ORDER BY name", - )) as Contact[]; + )) as ContactMaybeWithJsonStrings[]; + + return this.$normalizeContacts(rawContacts); }, /** @@ -1026,7 +1092,13 @@ export const PlatformServiceMixin = { Object.entries(changes).forEach(([key, value]) => { if (value !== undefined) { setParts.push(`${key} = ?`); - params.push(value); + + // Handle contactMethods field - convert array to JSON string + if (key === "contactMethods" && Array.isArray(value)) { + params.push(JSON.stringify(value)); + } else { + params.push(value); + } } }); @@ -1048,45 +1120,36 @@ export const PlatformServiceMixin = { /** * Get all contacts as typed objects - $getAllContacts() * Eliminates verbose query + mapping patterns - * @returns Promise Array of contact objects + * Handles JSON string/object duality for contactMethods field + * @returns Promise Array of normalized contact objects */ async $getAllContacts(): Promise { - const results = await this.$dbQuery( - "SELECT did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl FROM contacts ORDER BY name", - ); + const rawContacts = (await this.$query( + "SELECT * FROM contacts ORDER BY name", + )) as ContactMaybeWithJsonStrings[]; - return this.$mapResults(results, (row: unknown[]) => ({ - did: row[0] as string, - name: row[1] as string, - publicKeyBase64: row[2] as string, - seesMe: Boolean(row[3]), - registered: Boolean(row[4]), - nextPubKeyHashB64: row[5] as string, - profileImageUrl: row[6] as string, - })); + return this.$normalizeContacts(rawContacts); }, /** * Get single contact by DID - $getContact() * Eliminates verbose single contact query patterns + * Handles JSON string/object duality for contactMethods field * @param did Contact DID to retrieve - * @returns Promise Contact object or null if not found + * @returns Promise Normalized contact object or null if not found */ async $getContact(did: string): Promise { - const results = await this.$dbQuery( + const rawContacts = (await this.$query( "SELECT * FROM contacts WHERE did = ?", [did], - ); + )) as ContactMaybeWithJsonStrings[]; - if (!results || !results.values || results.values.length === 0) { + if (rawContacts.length === 0) { return null; } - const contactData = this._mapColumnsToValues( - results.columns, - results.values, - ); - return contactData.length > 0 ? (contactData[0] as Contact) : null; + const normalizedContacts = this.$normalizeContacts(rawContacts); + return normalizedContacts[0]; }, /** @@ -1681,6 +1744,7 @@ declare module "@vue/runtime-core" { $contactCount(): Promise; $settings(defaults?: Settings): Promise; $accountSettings(did?: string, defaults?: Settings): Promise; + $normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[]; // Settings update shortcuts (eliminate 90% boilerplate) $saveSettings(changes: Partial): Promise; diff --git a/src/views/ContactEditView.vue b/src/views/ContactEditView.vue index 5b6f63a5..2c4cebfd 100644 --- a/src/views/ContactEditView.vue +++ b/src/views/ContactEditView.vue @@ -239,7 +239,21 @@ export default class ContactEditView extends Vue { this.contact = contact; this.contactName = contact.name || ""; this.contactNotes = contact.notes || ""; - this.contactMethods = contact.contactMethods || []; + + // Ensure contactMethods is a valid array of ContactMethod objects + if (Array.isArray(contact.contactMethods)) { + this.contactMethods = contact.contactMethods.filter((method) => { + return ( + method && + typeof method === "object" && + typeof method.label === "string" && + typeof method.type === "string" && + typeof method.value === "string" + ); + }); + } else { + this.contactMethods = []; + } } else { this.notify.error( `${NOTIFY_CONTACT_NOT_FOUND.message} ${contactDid}`,