diff --git a/BUILDING.md b/BUILDING.md index ba5649da..e1e94fcd 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -8,8 +8,10 @@ This guide explains how to build TimeSafari for different platforms using the co ```bash # 🖥️ Web Development -npm run build:web:dev # Start development server with hot reload -npm run build:web:prod # Production build +npm install # setup -- and pkgx.dev `dev` command before this will set environment with npm, etc +npm run build:web:serve -- --test # Start with test endorser server +npm run build:web:dev # Start development server with hot reload with local endorser server +npm run build:web:prod # Production build # 📱 Mobile Development npm run build:ios # iOS build (opens Xcode) @@ -2401,4 +2403,4 @@ All scripts use consistent error handling: --- -**Note**: This documentation is maintained alongside the build system. For the most up-to-date information, refer to the actual script files and Vite configuration files in the repository. \ No newline at end of file +**Note**: This documentation is maintained alongside the build system. For the most up-to-date information, refer to the actual script files and Vite configuration files in the repository. diff --git a/README.md b/README.md index 6d987467..59290d4d 100644 --- a/README.md +++ b/README.md @@ -3,36 +3,9 @@ [Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude and expand to crowd-fund with time & money, then record and see the impact of contributions. -## Database Migration Status - -**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place. - -### Migration Progress -- ✅ **SQLite Database Service**: Fully implemented with absurd-sql -- ✅ **Platform Service Layer**: Unified database interface across platforms -- ✅ **Settings Migration**: Core user settings transferred -- ✅ **Account Migration**: Identity and key management -- 🔄 **Contact Migration**: User contact data (via import interface) -- 📋 **Code Cleanup**: Remove unused Dexie imports - -### Migration Fence -The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details. - -**Key Points**: -- Legacy Dexie database is disabled by default -- All database operations go through `PlatformServiceMixin` -- Migration tools provide controlled access to both databases -- Clear separation between legacy and new code - -### Migration Documentation -- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process -- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules -- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools - ## Roadmap -See [project.task.yaml](project.task.yaml) for current priorities. -(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.) +See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e2ba0) for current priorities. ## Setup & Building @@ -42,14 +15,16 @@ Quick start: ```bash npm install -npm run dev +npm run build:web:serve -- --test ``` +To be able to make submissions: go to "profile" (bottom left), go to the bottom and expand "Show Advanced Settings", go to the bottom and to the "Test Page", and finally "Become User 0" to see all the functionality. + See [BUILDING.md](BUILDING.md) for comprehensive build instructions for all platforms (Web, Electron, iOS, Android, Docker). ## Development Database Clearing -TimeSafari provides a simple script-based approach to clear the database for development purposes. +TimeSafari provides a simple script-based approach to clear the local database (not the claim server) for development purposes. ### Quick Usage ```bash @@ -126,7 +101,6 @@ 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 ## Tests diff --git a/package-lock.json b/package-lock.json index 6d19bd08..5b40d345 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.0.6", + "version": "1.0.7-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.0.6", + "version": "1.0.7-beta", "dependencies": { "@capacitor-community/electron": "^5.0.1", "@capacitor-community/sqlite": "6.0.2", diff --git a/package.json b/package.json index cd34bc17..53e3f5b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "1.0.6", + "version": "1.0.7-beta", "description": "Time Safari Application", "author": { "name": "Time Safari Team" diff --git a/src/components/ContactInputForm.vue b/src/components/ContactInputForm.vue index 35c693e4..dbbc1485 100644 --- a/src/components/ContactInputForm.vue +++ b/src/components/ContactInputForm.vue @@ -167,7 +167,7 @@ export default class ContactInputForm extends Vue { */ @Emit("qr-scan") private handleQRScan(): void { - console.log("[ContactInputForm] QR scan button clicked"); + // QR scan button clicked - event emitted for parent handling } } diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index e28b3ac9..735252f7 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1348,12 +1348,12 @@ export async function createEndorserJwtVcFromClaim( } /** - * Create a JWT for a RegisterAction claim. + * Create a JWT for a RegisterAction claim, used for registrations & invites. * * @param activeDid - The DID of the user creating the invite - * @param contact - The contact to register, with a 'did' field (all optional for invites) - * @param identifier - The identifier for the invite, usually random - * @param expiresIn - The number of seconds until the invite expires + * @param contact - Optional - The contact to register, with a 'did' field (all optional for invites) + * @param identifier - Optional - The identifier for the invite, usually random + * @param expiresIn - Optional - The number of seconds until the invite expires * @returns The JWT for the RegisterAction claim */ export async function createInviteJwt( @@ -1367,7 +1367,7 @@ export async function createInviteJwt( "@type": "RegisterAction", agent: { identifier: activeDid }, object: SERVICE_ID, - identifier: identifier, + identifier: identifier, // not sent if undefined }; if (contact?.did) { vcClaim.participant = { identifier: contact.did }; diff --git a/src/router/index.ts b/src/router/index.ts index 7d1e738c..043d3d0c 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -82,6 +82,15 @@ const routes: Array = [ name: "database-migration", component: () => import("../views/DatabaseMigration.vue"), }, + { + path: "/deep-link-error", + name: "deep-link-error", + component: () => import("../views/DeepLinkErrorView.vue"), + meta: { + title: "Invalid Deep Link", + requiresAuth: false, + }, + }, { path: "/did/:did?", name: "did", @@ -276,15 +285,6 @@ const routes: Array = [ name: "user-profile", component: () => import("../views/UserProfileView.vue"), }, - { - path: "/deep-link-error", - name: "deep-link-error", - component: () => import("../views/DeepLinkErrorView.vue"), - meta: { - title: "Invalid Deep Link", - requiresAuth: false, - }, - }, ]; const isElectron = window.location.protocol === "file:"; diff --git a/src/services/AbsurdSqlDatabaseService.ts b/src/services/AbsurdSqlDatabaseService.ts index 707513e5..5d2ab7ad 100644 --- a/src/services/AbsurdSqlDatabaseService.ts +++ b/src/services/AbsurdSqlDatabaseService.ts @@ -1,7 +1,9 @@ // **WORKER-COMPATIBLE CRYPTO POLYFILL**: Must be at the very top // This prevents "crypto is not defined" errors when running in worker context if (typeof window === "undefined" && typeof crypto === "undefined") { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).crypto = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any getRandomValues: (array: any) => { // Simple fallback for worker context for (let i = 0; i < array.length; i++) { diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 62ae7b15..d8445607 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -179,7 +179,7 @@ export class DeepLinkHandler { const validRoute = routeSchema.parse(path) as DeepLinkRoute; routeName = ROUTE_MAP[validRoute].name; } catch (error) { - console.error(`[DeepLink] Invalid route path: ${path}`); + logger.error(`[DeepLink] Invalid route path: ${path}`); // Redirect to error page with information about the invalid link await this.router.replace({ diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts index 7ec13c01..a731ee22 100644 --- a/src/services/platforms/WebPlatformService.ts +++ b/src/services/platforms/WebPlatformService.ts @@ -693,7 +693,8 @@ export class WebPlatformService implements PlatformService { const setClause = keys.map((key) => `${key} = ?`).join(", "); const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`; const params = [...keys.map((key) => settings[key]), did]; - console.log( + // Log update operation for debugging + logger.debug( "[WebPlatformService] updateDidSpecificSettings", sql, JSON.stringify(params, null, 2), diff --git a/src/test/PlatformServiceMixinTest.vue b/src/test/PlatformServiceMixinTest.vue index f54eea11..219c72cf 100644 --- a/src/test/PlatformServiceMixinTest.vue +++ b/src/test/PlatformServiceMixinTest.vue @@ -92,6 +92,7 @@ import { MASTER_SETTINGS_KEY } from "@/db/tables/settings"; }) export default class PlatformServiceMixinTest extends Vue { result: string = ""; + // eslint-disable-next-line @typescript-eslint/no-explicit-any userZeroTestResult: any = null; activeTest: string = ""; // Track which test is currently active @@ -267,6 +268,7 @@ This tests the complete save → retrieve cycle with actual database interaction this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`; } catch (error) { this.result = `Error testing User #0 settings: ${error}`; + // eslint-disable-next-line no-console console.error("Error testing User #0 settings:", error); } } diff --git a/src/test/index.ts b/src/test/index.ts index 28b0e349..8974713b 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,9 +1,29 @@ import axios from "axios"; import * as didJwt from "did-jwt"; import { SERVICE_ID } from "../libs/endorserServer"; -import { deriveAddress, newIdentifier } from "../libs/crypto"; +import { + DEFAULT_ROOT_DERIVATION_PATH, + deriveAddress, + newIdentifier, +} from "../libs/crypto"; import { logger } from "../utils/logger"; import { AppString } from "../constants/app"; +import { saveNewIdentity } from "@/libs/util"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; + +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"; + +export async function testBecomeUser0() { + const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC); + + const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); + await saveNewIdentity(identity0, TEST_USER_0_MNEMONIC, DEFAULT_ROOT_DERIVATION_PATH); + const platformService = await PlatformServiceFactory.getInstance(); + await platformService.updateDidSpecificSettings(identity0.did, { + isRegistered: true, + }); +} /** * Get User #0 to sign & submit a RegisterAction for the user's activeDid. @@ -15,10 +35,7 @@ import { AppString } from "../constants/app"; * @throws Error if registration fails or database access fails */ export async function testServerRegisterUser() { - const testUser0Mnem = - "seminar accuse mystery assist delay law thing deal image undo guard initial shallow wrestle list fragile borrow velvet tomorrow awake explain test offer control"; - - const [addr, privateHex, publicHex, deriPath] = deriveAddress(testUser0Mnem); + const [addr, privateHex, publicHex, deriPath] = deriveAddress(TEST_USER_0_MNEMONIC); const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath); @@ -32,9 +49,9 @@ export async function testServerRegisterUser() { const vcClaim = { "@context": "https://schema.org", "@type": "RegisterAction", - agent: { did: identity0.did }, + agent: { identifier: identity0.did }, object: SERVICE_ID, - participant: { did: settings.activeDid }, + participant: { identifier: settings.activeDid }, }; // Make a payload for the claim @@ -71,4 +88,5 @@ export async function testServerRegisterUser() { const resp = await axios.post(url, payload, { headers }); logger.log("User registration result:", resp); + return resp; } diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 361a2ff2..68c09720 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -133,6 +133,7 @@ export const PlatformServiceMixin = { * Used for change detection and component updates */ currentActiveDid(): string | null { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return (this as any)._currentActiveDid; }, @@ -200,7 +201,9 @@ export const PlatformServiceMixin = { * This method should be called when the user switches identities */ async $updateActiveDid(newDid: string | null): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const oldDid = (this as any)._currentActiveDid; + // eslint-disable-next-line @typescript-eslint/no-explicit-any (this as any)._currentActiveDid = newDid; if (newDid !== oldDid) { @@ -291,6 +294,7 @@ export const PlatformServiceMixin = { // Convert searchBoxes array to JSON string if present if (settings.searchBoxes !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any (converted as any).searchBoxes = Array.isArray(settings.searchBoxes) ? JSON.stringify(settings.searchBoxes) : String(settings.searchBoxes); @@ -692,6 +696,7 @@ export const PlatformServiceMixin = { typeof method.value === "string"; if (!isValid && method !== undefined) { + // eslint-disable-next-line no-console console.warn( "[ContactNormalization] Invalid contact method:", method, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 892e1ebc..81c2978e 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -61,7 +61,8 @@ /> - +

- +

-
+
+

User Registration

+ + +
+ +

Notiwind Alerts

@@ -99,7 +109,7 @@
Register Passkey -
@@ -235,6 +245,7 @@ import { registerAndSavePasskey, SHARED_PHOTO_BASE64_KEY, } from "../libs/util"; +import { testBecomeUser0, testServerRegisterUser } from "@/test"; import { logger } from "../utils/logger"; import { Account } from "../db/tables/accounts"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; @@ -300,6 +311,7 @@ export default class Help extends Vue { // for passkeys credIdHex?: string; activeDid?: string; + apiServer?: string; jwt?: string; peerSetup?: PeerSetup; userName?: string; @@ -521,17 +533,6 @@ export default class Help extends Vue { ]; } - /** - * Method to trigger notification test - * Centralizes notification testing logic - */ - triggerTestNotification(config: { - notification: NotificationIface; - timeout?: number; - }) { - this.$notify(config.notification, config.timeout); - } - /** * Component initialization * @@ -541,6 +542,7 @@ export default class Help extends Vue { async mounted() { const settings = await this.$accountSettings(); this.activeDid = settings.activeDid || ""; + this.apiServer = settings.apiServer || ""; this.userName = settings.firstName; const account = await retrieveAccountMetadata(this.activeDid); @@ -553,6 +555,43 @@ export default class Help extends Vue { } } + /** + * Checks if running on production server + * + * @returns True if not on production server (enables test utilities) + */ + public isNotProdServer() { + return this.apiServer !== AppString.PROD_ENDORSER_API_SERVER; + } + + async registerMe() { + const response = await testServerRegisterUser(); + if (response.status === 201) { + alert("Registration successful."); + this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change + } else { + logger.error("Registration failure response:", response); + alert("Registration failed: " + (response.data.error || response.data)); + } + } + + async becomeUser0() { + await testBecomeUser0(); + alert("You are now User 0."); + this.$router.push({ name: "home" }); // because this page checks for registered status and sets things if it detects a change + } + + /** + * Method to trigger notification test + * Centralizes notification testing logic + */ + triggerTestNotification(config: { + notification: NotificationIface; + timeout?: number; + }) { + this.$notify(config.notification, config.timeout); + } + /** * Handles file upload for image sharing tests * @@ -609,7 +648,7 @@ export default class Help extends Vue { * Includes validation and user confirmation workflow * Uses notification helpers for consistent messaging */ - public async register() { + public async registerPasskey() { const DEFAULT_USERNAME = AppString.APP_NAME + " Tester"; if (!this.userName) { const modalConfig = createPasskeyNameModal(