Merge branch 'build-improvement' into performance-optimizations-testing
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
To change the production domain for all sharing functionality:
|
||||
The system uses environment variables to configure domains:
|
||||
|
||||
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 ...
|
||||
}
|
||||
```bash
|
||||
# Development
|
||||
VITE_APP_SERVER=http://localhost:8080
|
||||
|
||||
# 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";
|
||||
|
||||
// Sharing always uses production URLs
|
||||
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
### ✅ Consistent User Experience
|
||||
### ✅ Simplified Configuration
|
||||
|
||||
- All shared links work for all users regardless of environment
|
||||
- No more broken localhost links in development
|
||||
- Consistent behavior across all platforms
|
||||
- Single constant for all URL generation
|
||||
- No confusion about which constant to use
|
||||
- Consistent behavior across all functionality
|
||||
|
||||
### ✅ Environment Flexibility
|
||||
|
||||
- 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
|
||||
@@ -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 =
|
||||
|
||||
@@ -32,6 +32,17 @@ export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
|
||||
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<Contact, "contactMethods"> & {
|
||||
contactMethods?: string | Array<ContactMethod>;
|
||||
};
|
||||
|
||||
export const ContactSchema = {
|
||||
contacts: "&did, name", // no need to key by other things
|
||||
};
|
||||
|
||||
@@ -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<Contact[]> Array of contact objects
|
||||
* Handles JSON string/object duality for contactMethods field
|
||||
* @returns Promise<Contact[]> Array of normalized contact objects
|
||||
*/
|
||||
async $contacts(): Promise<Contact[]> {
|
||||
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<Contact[]> Array of contact objects
|
||||
* Handles JSON string/object duality for contactMethods field
|
||||
* @returns Promise<Contact[]> Array of normalized contact objects
|
||||
*/
|
||||
async $getAllContacts(): Promise<Contact[]> {
|
||||
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 | null> Contact object or null if not found
|
||||
* @returns Promise<Contact | null> Normalized contact object or null if not found
|
||||
*/
|
||||
async $getContact(did: string): Promise<Contact | null> {
|
||||
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<number>;
|
||||
$settings(defaults?: Settings): Promise<Settings>;
|
||||
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
||||
$normalizeContacts(rawContacts: ContactMaybeWithJsonStrings[]): Contact[];
|
||||
|
||||
// Settings update shortcuts (eliminate 90% boilerplate)
|
||||
$saveSettings(changes: Partial<Settings>): Promise<boolean>;
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user