From 861408c7bc38fa16d17658d60e3024f2b7b7593f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 3 Jul 2025 17:01:08 -0600 Subject: [PATCH 01/77] Consolidate deep-link paths to be derived from the same source so they don't get out of sync any more. --- src/interfaces/deepLinks.ts | 53 ++++++++++++++----------------- src/services/deepLinks.ts | 56 ++++++++++++++++++--------------- src/views/DeepLinkErrorView.vue | 2 +- 3 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index a275eecf..d9dbdbcc 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -27,37 +27,8 @@ */ import { z } from "zod"; -// Add a union type of all valid route paths -export const VALID_DEEP_LINK_ROUTES = [ - // note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts - "claim", - "claim-add-raw", - "claim-cert", - "confirm-gift", - "contact-import", - "did", - "invite-one-accept", - "onboard-meeting-setup", - "project", - "user-profile", -] as const; - -// Create a type from the array -export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; - -// Update your schema definitions to use this type -export const baseUrlSchema = z.object({ - scheme: z.literal("timesafari"), - path: z.string(), - queryParams: z.record(z.string()).optional(), -}); - -// Use the type to ensure route validation -export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES); - // Parameter validation schemas for each route type export const deepLinkSchemas = { - // note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts claim: z.object({ id: z.string(), }), @@ -72,16 +43,22 @@ export const deepLinkSchemas = { "confirm-gift": z.object({ id: z.string(), }), + "contact-edit": z.object({ + did: z.string(), + }), "contact-import": z.object({ jwt: z.string(), }), + contacts: z.object({ + contacts: z.string(), // JSON string of contacts array + }), did: z.object({ did: z.string(), }), "invite-one-accept": z.object({ jwt: z.string(), }), - "onboard-meeting-setup": z.object({ + "onboard-meeting-members": z.object({ id: z.string(), }), project: z.object({ @@ -92,6 +69,19 @@ export const deepLinkSchemas = { }), }; +// Create a type from the array +export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number]; + +// Update your schema definitions to use this type +export const baseUrlSchema = z.object({ + scheme: z.literal("timesafari"), + path: z.string(), + queryParams: z.record(z.string()).optional(), +}); + +// Add a union type of all valid route paths +export const VALID_DEEP_LINK_ROUTES = Object.keys(deepLinkSchemas) as readonly (keyof typeof deepLinkSchemas)[]; + export type DeepLinkParams = { [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; }; @@ -100,3 +90,6 @@ export interface DeepLinkError extends Error { code: string; details?: unknown; } + +// Use the type to ensure route validation +export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES as [string, ...string[]]); diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index bff3346c..34d35cbb 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -44,6 +44,8 @@ */ import { Router } from "vue-router"; +import { z } from "zod"; + import { deepLinkSchemas, baseUrlSchema, @@ -53,6 +55,31 @@ import { import { logConsoleAndDb } from "../db/databaseUtil"; import type { DeepLinkError } from "../interfaces/deepLinks"; +// Helper function to extract the first key from a Zod object schema +function getFirstKeyFromZodObject(schema: z.ZodObject): string | undefined { + const shape = schema.shape; + const keys = Object.keys(shape); + return keys.length > 0 ? keys[0] : undefined; +} + +/** + * Maps deep link routes to their corresponding Vue router names and optional parameter keys. + * + * It's an object where keys are the deep link routes and values are objects with 'name' and 'paramKey'. + * + * The paramKey is used to extract the parameter from the route path, + * because "router.replace" expects the right parameter name for the route. + */ +export const ROUTE_MAP: Record = + Object.entries(deepLinkSchemas).reduce((acc, [routeName, schema]) => { + const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject); + acc[routeName] = { + name: routeName, + paramKey + }; + return acc; + }, {} as Record); + /** * Handles processing and routing of deep links in the application. * Provides validation, error handling, and routing for deep link URLs. @@ -69,30 +96,7 @@ export class DeepLinkHandler { } /** - * Maps deep link routes to their corresponding Vue router names and optional parameter keys. - * - * The paramKey is used to extract the parameter from the route path, - * because "router.replace" expects the right parameter name for the route. - * The default is "id". - */ - private readonly ROUTE_MAP: Record< - string, - { name: string; paramKey?: string } - > = { - // note that similar lists are in src/interfaces/deepLinks.ts - claim: { name: "claim" }, - "claim-add-raw": { name: "claim-add-raw" }, - "claim-cert": { name: "claim-cert" }, - "confirm-gift": { name: "confirm-gift" }, - "contact-import": { name: "contact-import", paramKey: "jwt" }, - did: { name: "did", paramKey: "did" }, - "invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" }, - "onboard-meeting-members": { name: "onboard-meeting-members" }, - project: { name: "project" }, - "user-profile": { name: "user-profile" }, - }; - /** * Parses deep link URL into path, params and query components. * Validates URL structure using Zod schemas. * @@ -126,7 +130,7 @@ export class DeepLinkHandler { // ); // Validate route exists before proceeding - if (!this.ROUTE_MAP[routePath]) { + if (!ROUTE_MAP[routePath]) { throw { code: "INVALID_ROUTE", message: `Invalid route path: ${routePath}`, @@ -144,7 +148,7 @@ export class DeepLinkHandler { const params: Record = {}; if (pathParams) { // Now we know routePath exists in ROUTE_MAP - const routeConfig = this.ROUTE_MAP[routePath]; + const routeConfig = ROUTE_MAP[routePath]; params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); } return { path: routePath, params, query }; @@ -170,7 +174,7 @@ export class DeepLinkHandler { try { // Validate route exists const validRoute = routeSchema.parse(path) as DeepLinkRoute; - routeName = this.ROUTE_MAP[validRoute].name; + routeName = ROUTE_MAP[validRoute].name; } catch (error) { // Log the invalid route attempt logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true); diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue index e65d3b58..3abb3ea1 100644 --- a/src/views/DeepLinkErrorView.vue +++ b/src/views/DeepLinkErrorView.vue @@ -93,7 +93,7 @@ const reportIssue = () => { // Log the error for analytics onMounted(() => { logConsoleAndDb( - `[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}`, + `[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`, true, ); }); From b0d99e7c1e218aea4abee55055fb7ab4a2b65816 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 12 Jul 2025 20:17:38 -0600 Subject: [PATCH 02/77] fix: quick-and-dirty fix to get the correct environment variables --- README.md | 2 +- vite.config.common.mts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d673c279..b3559d1b 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions. Application icons are in the `assets` directory, processed by the `capacitor-assets` command. -To add a Font Awesome icon, add to main.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name. +To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name. ## Other diff --git a/vite.config.common.mts b/vite.config.common.mts index 6b238be9..355c377f 100644 --- a/vite.config.common.mts +++ b/vite.config.common.mts @@ -6,7 +6,9 @@ import path from "path"; import { fileURLToPath } from 'url'; // Load environment variables -dotenv.config(); +console.log('NODE_ENV:', process.env.NODE_ENV) +dotenv.config({ path: `.env.${process.env.NODE_ENV}` }) + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); From a9a8ba217cd6015321911e98e6843e988dc2c4ae Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 12 Jul 2025 22:10:07 -0600 Subject: [PATCH 03/77] bump to version 1.0.3 build 36 --- BUILDING.md | 5 +++-- CHANGELOG.md | 5 ++++- android/app/build.gradle | 4 ++-- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 04531f01..81ff3d1d 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -362,7 +362,7 @@ Prerequisites: macOS with Xcode installed 4. Bump the version to match Android & package.json: ``` - cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd - + cd ios/App && xcrun agvtool new-version 36 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.3;/g" App.xcodeproj/project.pbxproj && cd - # Unfortunately this edits Info.plist directly. #xcrun agvtool new-marketing-version 0.4.5 ``` @@ -385,11 +385,12 @@ Prerequisites: macOS with Xcode installed * This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly. * If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`). * Click Distribute -> App Store Connect - * In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build. + * In AppStoreConnect, add the build to the distribution. You may have to remove the current build with the "-" when you hover over it, then "Add Build" with the new build. * May have to go to App Review, click Submission, then hover over the build and click "-". * It can take 15 minutes for the build to show up in the list of builds. * You'll probably have to "Manage" something about encryption, disallowed in France. * Then "Save" and "Add to Review" and "Resubmit to App Review". + * Eventually it'll be "Ready for Distribution" which means ### Android Build diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c9eb2a7..00662f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [1.0.3] - 2025.07.12 ### Changed - Photo is pinned to profile mode +### Fixed +- Deep link URLs (and other prod settings) +- Error in BVC begin view ## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d diff --git a/android/app/build.gradle b/android/app/build.gradle index 0843dce8..ddad5b5d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "app.timesafari.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 35 - versionName "1.0.2" + versionCode 36 + versionName "1.0.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 01ed14fe..a3afcb31 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -430,7 +430,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 35; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package-lock.json b/package-lock.json index a0f90964..aa41ccab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.0.3-beta", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.0.3-beta", + "version": "1.0.3", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index a2152348..294f2174 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "1.0.3-beta", + "version": "1.0.3", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From dc21e8dac3440d716e64299818cbac5fb26ad5a7 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 12 Jul 2025 22:10:53 -0600 Subject: [PATCH 04/77] bump version number and add '-beta' --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa41ccab..3b183e19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.0.3", + "version": "1.0.4-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.0.3", + "version": "1.0.4-beta", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 294f2174..13e52ae8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "1.0.3", + "version": "1.0.4-beta", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From 33ce6bdb72cafa8b7c48c0c90d7998e32ca3c7f4 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 14 Jul 2025 20:49:40 -0600 Subject: [PATCH 05/77] fix: invite-one-accept deep link would not route properly --- src/interfaces/deepLinks.ts | 6 ++- src/main.capacitor.ts | 11 +++-- src/router/index.ts | 13 +++--- src/services/deepLinks.ts | 68 +++++++++++++++++-------------- src/views/DeepLinkErrorView.vue | 15 +++++-- src/views/InviteOneAcceptView.vue | 2 +- 6 files changed, 66 insertions(+), 49 deletions(-) diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index d9dbdbcc..f5838dc2 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -50,13 +50,15 @@ export const deepLinkSchemas = { jwt: z.string(), }), contacts: z.object({ - contacts: z.string(), // JSON string of contacts array + contactJwt: z.string().optional(), + inviteJwt: z.string().optional(), }), did: z.object({ did: z.string(), }), "invite-one-accept": z.object({ - jwt: z.string(), + // optional because A) it could be a query param, and B) the page displays an input if things go wrong + jwt: z.string().optional(), }), "onboard-meeting-members": z.object({ id: z.string(), diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index 3ac12d1f..42f1c38b 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -72,12 +72,11 @@ const handleDeepLink = async (data: { url: string }) => { await deepLinkHandler.handleDeepLink(data.url); } catch (error) { logger.error("[DeepLink] Error handling deep link: ", error); - handleApiError( - { - message: error instanceof Error ? error.message : safeStringify(error), - } as AxiosError, - "deep-link", - ); + let message: string = error instanceof Error ? error.message : safeStringify(error); + if (data.url) { + message += `\nURL: ${data.url}`; + } + handleApiError({ message } as AxiosError, "deep-link"); } }; diff --git a/src/router/index.ts b/src/router/index.ts index 010972bf..e43e104b 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -73,6 +73,11 @@ const routes: Array = [ name: "contacts", component: () => import("../views/ContactsView.vue"), }, + { + path: "/database-migration", + name: "database-migration", + component: () => import("../views/DatabaseMigration.vue"), + }, { path: "/did/:did?", name: "did", @@ -139,8 +144,9 @@ const routes: Array = [ component: () => import("../views/InviteOneView.vue"), }, { + // optional because A) it could be a query param, and B) the page displays an input if things go wrong path: "/invite-one-accept/:jwt?", - name: "InviteOneAcceptView", + name: "invite-one-accept", component: () => import("../views/InviteOneAcceptView.vue"), }, { @@ -148,11 +154,6 @@ const routes: Array = [ name: "logs", component: () => import("../views/LogView.vue"), }, - { - path: "/database-migration", - name: "database-migration", - component: () => import("../views/DatabaseMigration.vue"), - }, { path: "/new-activity", name: "new-activity", diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 34d35cbb..8cd6db88 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -119,15 +119,6 @@ export class DeepLinkHandler { const [path, queryString] = parts[1].split("?"); const [routePath, ...pathParams] = path.split("/"); - // logger.info( - // "[DeepLink] Debug:", - // "Route Path:", - // routePath, - // "Path Params:", - // pathParams, - // "Query String:", - // queryString, - // ); // Validate route exists before proceeding if (!ROUTE_MAP[routePath]) { @@ -151,6 +142,11 @@ export class DeepLinkHandler { const routeConfig = ROUTE_MAP[routePath]; params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); } + + // logConsoleAndDb( + // `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`, + // false, + // ); return { path: routePath, params, query }; } @@ -182,52 +178,65 @@ export class DeepLinkHandler { // Redirect to error page with information about the invalid link await this.router.replace({ name: "deep-link-error", + params, query: { originalPath: path, errorCode: "INVALID_ROUTE", - message: `The link you followed (${path}) is not supported`, + errorMessage: `The link you followed (${path}) is not supported`, + ...query, }, }); - throw { - code: "INVALID_ROUTE", - message: `Unsupported route: ${path}`, - }; + // This previously threw an error but we're redirecting so there's no need. + return; } // Continue with parameter validation as before... const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; + let validatedParams, validatedQuery; try { - const validatedParams = await schema.parseAsync({ - ...params, - ...query, + validatedParams = await schema.parseAsync(params); + validatedQuery = await schema.parseAsync(query); + } catch (error) { + // For parameter validation errors, provide specific error feedback + logConsoleAndDb(`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`, true); + await this.router.replace({ + name: "deep-link-error", + params, + query: { + originalPath: path, + errorCode: "INVALID_PARAMETERS", + errorMessage: `The link parameters are invalid: ${(error as Error).message}`, + ...query, + }, }); + // This previously threw an error but we're redirecting so there's no need. + return; + } + + try { await this.router.replace({ name: routeName, params: validatedParams, - query, + query: validatedQuery, }); } catch (error) { + logConsoleAndDb(`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)} ... and validated query: ${JSON.stringify(validatedQuery)}`, true); // For parameter validation errors, provide specific error feedback await this.router.replace({ name: "deep-link-error", + params: validatedParams, query: { originalPath: path, - errorCode: "INVALID_PARAMETERS", - message: `The link parameters are invalid: ${(error as Error).message}`, + errorCode: "ROUTING_ERROR", + errorMessage: `Error routing to ${routeName}: ${(JSON.stringify(error))}`, + ...validatedQuery, }, }); - - throw { - code: "INVALID_PARAMETERS", - message: (error as Error).message, - details: error, - params: params, - query: query, - }; } + } /** @@ -239,7 +248,6 @@ export class DeepLinkHandler { */ async handleDeepLink(url: string): Promise { try { - logConsoleAndDb("[DeepLink] Processing URL: " + url, false); const { path, params, query } = this.parseDeepLink(url); // Ensure params is always a Record by converting undefined to empty string const sanitizedParams = Object.fromEntries( @@ -249,7 +257,7 @@ export class DeepLinkHandler { } catch (error) { const deepLinkError = error as DeepLinkError; logConsoleAndDb( - `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`, + `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`, true, ); diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue index 3abb3ea1..f1c47f37 100644 --- a/src/views/DeepLinkErrorView.vue +++ b/src/views/DeepLinkErrorView.vue @@ -31,7 +31,7 @@

Supported Deep Links

  • - timesafari://{{ routeItem }}/:id + timesafari://{{ routeItem }}/:{{ deepLinkSchemaKeys[routeItem] }}
@@ -41,12 +41,19 @@ diff --git a/src/components/ContactInputForm.vue b/src/components/ContactInputForm.vue index 8d791eda..35c693e4 100644 --- a/src/components/ContactInputForm.vue +++ b/src/components/ContactInputForm.vue @@ -64,7 +64,7 @@ diff --git a/src/components/ContactListHeader.vue b/src/components/ContactListHeader.vue index 3e2c4589..cfb65be2 100644 --- a/src/components/ContactListHeader.vue +++ b/src/components/ContactListHeader.vue @@ -8,21 +8,21 @@ :checked="allContactsSelected" class="align-middle ml-2 h-6 w-6" data-testId="contactCheckAllTop" - @click="$emit('toggle-all-selection')" + @click="emitToggleAllSelection" /> @@ -33,7 +33,7 @@ v-if="showGiveNumbers" class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md" :class="giveAmountsButtonClass" - @click="$emit('toggle-give-totals')" + @click="emitToggleGiveTotals" > {{ giveAmountsButtonText }} @@ -41,7 +41,7 @@ @@ -50,7 +50,7 @@ diff --git a/src/components/ContactListItem.vue b/src/components/ContactListItem.vue index abdfdbb0..c972fe80 100644 --- a/src/components/ContactListItem.vue +++ b/src/components/ContactListItem.vue @@ -9,14 +9,14 @@ :checked="isSelected" class="ml-2 h-6 w-6 flex-shrink-0" data-testId="contactCheckOne" - @click="$emit('toggle-selection', contact.did)" + @click="emitToggleSelection(contact.did)" />
@@ -63,7 +63,7 @@ @@ -71,7 +71,7 @@ @@ -81,7 +81,7 @@ @@ -102,7 +102,7 @@ diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue index 36db158a..d10d4847 100644 --- a/src/components/MembersList.vue +++ b/src/components/MembersList.vue @@ -178,7 +178,7 @@ // Validation: Passes lint checks and TypeScript compilation // Navigation: Contacts → Chair Icon → Start/Join Meeting → Members List -import { Component, Vue, Prop } from "vue-facing-decorator"; +import { Component, Vue, Prop, Emit } from "vue-facing-decorator"; import { errorStringForLog, @@ -222,6 +222,12 @@ export default class MembersList extends Vue { @Prop({ required: true }) password!: string; @Prop({ default: false }) showOrganizerTools!: boolean; + // Emit methods using @Emit decorator + @Emit("error") + emitError(message: string) { + return message; + } + decryptedMembers: DecryptedMember[] = []; firstName = ""; isLoading = true; @@ -262,10 +268,7 @@ export default class MembersList extends Vue { "Error fetching members: " + errorStringForLog(error), true, ); - this.$emit( - "error", - serverMessageForUser(error) || "Failed to fetch members.", - ); + this.emitError(serverMessageForUser(error) || "Failed to fetch members."); } finally { this.isLoading = false; } @@ -478,8 +481,7 @@ export default class MembersList extends Vue { "Error toggling admission: " + errorStringForLog(error), true, ); - this.$emit( - "error", + this.emitError( serverMessageForUser(error) || "Failed to update member admission status.", ); diff --git a/src/libs/util.ts b/src/libs/util.ts index 3696c5d7..ea234243 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -974,28 +974,28 @@ export async function importFromMnemonic( 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.insertDidSpecificSettings(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.info("[importFromMnemonic] Test User #0 settings verification", { did: newId.did, firstName, @@ -1003,40 +1003,50 @@ export async function importFromMnemonic( 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"); - + 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.info("[importFromMnemonic] Test User #0 settings after retry", { - firstName: retrySettings[0], - isRegistered: retrySettings[1], - }); + logger.info( + "[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"); + 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); + logger.error( + "[importFromMnemonic] Error setting up Test User #0 settings:", + error, + ); // Don't throw - allow the import to continue even if settings fail } } diff --git a/src/test/PlatformServiceMixinTest.vue b/src/test/PlatformServiceMixinTest.vue index f25d78a4..1ec225fa 100644 --- a/src/test/PlatformServiceMixinTest.vue +++ b/src/test/PlatformServiceMixinTest.vue @@ -3,16 +3,18 @@

PlatformServiceMixin Test

- -
+

User #0 Settings Test Result:

-
{{ JSON.stringify(userZeroTestResult, null, 2) }}
+
{{
+        JSON.stringify(userZeroTestResult, null, 2)
+      }}
{{ result }}
@@ -55,16 +57,16 @@ export default class PlatformServiceMixinTest extends Vue { try { // User #0's DID const userZeroDid = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F"; - + this.result = "Testing User #0 settings..."; - + // Test the debug methods await this.$debugMergedSettings(userZeroDid); - + // Get the actual settings const didSettings = await this.$debugDidSettings(userZeroDid); const accountSettings = await this.$accountSettings(userZeroDid); - + this.userZeroTestResult = { didSettings, accountSettings, @@ -72,7 +74,7 @@ export default class PlatformServiceMixinTest extends Vue { firstName: accountSettings.firstName, timestamp: new Date().toISOString(), }; - + this.result = `User #0 settings test completed. isRegistered: ${accountSettings.isRegistered}`; } catch (error) { this.result = `Error testing User #0 settings: ${error}`; diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts index 2abcdcf1..09fec9c0 100644 --- a/src/utils/PlatformServiceMixin.ts +++ b/src/utils/PlatformServiceMixin.ts @@ -87,7 +87,7 @@ interface VueComponentWithMixin { // VueComponentWithMixin, // Map> // >(); -// +// // /** // * Cache configuration constants // */ @@ -220,13 +220,20 @@ export const PlatformServiceMixin = { const obj: Record = {}; columns.forEach((column, index) => { let value = row[index]; - + // Convert SQLite integer booleans to JavaScript booleans - if (column === 'isRegistered' || column === 'finishedOnboarding' || - column === 'filterFeedByVisible' || column === 'filterFeedByNearby' || - column === 'hideRegisterPromptOnNewContact' || column === 'showContactGivesInline' || - column === 'showGeneralAdvanced' || column === 'showShortcutBvc' || - column === 'warnIfProdServer' || column === 'warnIfTestServer') { + if ( + column === "isRegistered" || + column === "finishedOnboarding" || + column === "filterFeedByVisible" || + column === "filterFeedByNearby" || + column === "hideRegisterPromptOnNewContact" || + column === "showContactGivesInline" || + column === "showGeneralAdvanced" || + column === "showShortcutBvc" || + column === "warnIfProdServer" || + column === "warnIfTestServer" + ) { if (value === 1) { value = true; } else if (value === 0) { @@ -234,7 +241,7 @@ export const PlatformServiceMixin = { } // Keep null values as null } - + obj[column] = value; }); return obj; @@ -244,7 +251,7 @@ export const PlatformServiceMixin = { /** * Self-contained implementation of parseJsonField * Safely parses JSON strings with fallback to default value - * + * * Consolidate this with src/libs/util.ts parseJsonField */ _parseJsonField(value: unknown, defaultValue: T): T { @@ -1403,7 +1410,9 @@ export const PlatformServiceMixin = { ); if (!result?.values?.length) { - logger.warn(`[PlatformServiceMixin] No settings found for DID: ${did}`); + logger.warn( + `[PlatformServiceMixin] No settings found for DID: ${did}`, + ); return null; } @@ -1413,7 +1422,9 @@ export const PlatformServiceMixin = { ); if (!mappedResults.length) { - logger.warn(`[PlatformServiceMixin] Failed to map settings for DID: ${did}`); + logger.warn( + `[PlatformServiceMixin] Failed to map settings for DID: ${did}`, + ); return null; } @@ -1428,7 +1439,10 @@ export const PlatformServiceMixin = { return settings; } catch (error) { - logger.error(`[PlatformServiceMixin] Error debugging settings for DID ${did}:`, error); + logger.error( + `[PlatformServiceMixin] Error debugging settings for DID ${did}:`, + error, + ); return null; } }, @@ -1442,14 +1456,24 @@ export const PlatformServiceMixin = { async $debugMergedSettings(did: string): Promise { try { // Get default settings - const defaultSettings = await this.$getSettings(MASTER_SETTINGS_KEY, {}); - logger.info(`[PlatformServiceMixin] Default settings:`, defaultSettings); + const defaultSettings = await this.$getSettings( + MASTER_SETTINGS_KEY, + {}, + ); + logger.info( + `[PlatformServiceMixin] Default settings:`, + defaultSettings, + ); // Get DID-specific settings const didSettings = await this.$debugDidSettings(did); // Get merged settings - const mergedSettings = await this.$getMergedSettings(MASTER_SETTINGS_KEY, did, defaultSettings || {}); + const mergedSettings = await this.$getMergedSettings( + MASTER_SETTINGS_KEY, + did, + defaultSettings || {}, + ); logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, { defaultSettings, @@ -1458,7 +1482,10 @@ export const PlatformServiceMixin = { isRegistered: mergedSettings.isRegistered, }); } catch (error) { - logger.error(`[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`, error); + logger.error( + `[PlatformServiceMixin] Error debugging merged settings for DID ${did}:`, + error, + ); } }, }, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index ef35f800..1cb9be94 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1415,7 +1415,8 @@ export default class AccountViewView extends Vue { return; } } catch (error) { - this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS; + this.limitsMessage = + ACCOUNT_VIEW_CONSTANTS.LIMITS.ERROR_RETRIEVING_LIMITS; logger.error("Error retrieving limits: ", error); // this.notify.error(this.limitsMessage, TIMEOUTS.STANDARD); } finally { @@ -1475,7 +1476,7 @@ export default class AccountViewView extends Vue { async deleteImage(): Promise { try { // Extract the image ID from the full URL - const imageId = this.profileImageUrl?.split('/').pop(); + const imageId = this.profileImageUrl?.split("/").pop(); if (!imageId) { this.notify.error("Invalid image URL"); return; From 118e93b85afb8b222b25c48d55b6de73762e4a5c Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 30 Jul 2025 15:45:59 +0800 Subject: [PATCH 18/77] Fix: invalid clean command --- scripts/build-ios.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh index b4ee52e3..985fb080 100755 --- a/scripts/build-ios.sh +++ b/scripts/build-ios.sh @@ -186,8 +186,8 @@ clean_ios_build() { log_debug "Cleaned ios/App/DerivedData/" fi - # Clean Capacitor - npx cap clean ios || true + # Clean Capacitor (using npm script instead of invalid cap clean command) + npm run clean:ios || true log_success "iOS build cleaned" } From 9067bec54aec277663d046aefae69be72dae370f Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 30 Jul 2025 09:48:52 +0000 Subject: [PATCH 19/77] fix: Convert searchBoxes arrays to JSON strings in $saveSettings and $updateSettings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _convertSettingsForStorage helper method to handle Settings → SettingsWithJsonStrings conversion - Fix $saveSettings and $saveUserSettings to properly convert searchBoxes arrays to JSON strings before database storage - Update SearchAreaView.vue to use array format instead of manual JSON.stringify conversion - Add comprehensive test UI in PlatformServiceMixinTest.vue with visual feedback and clear demonstration of conversion process - Document migration strategy for consolidating $updateSettings into $saveSettings to reduce code duplication - Add deprecation notices to $updateSettings method with clear migration guidance The fix ensures that searchBoxes arrays are properly converted to JSON strings before database storage, preventing data corruption and maintaining consistency with the SettingsWithJsonStrings type definition. The enhanced test interface provides clear visualization of the conversion process and database storage format. Migration Strategy: - $saveSettings: ✅ KEEP (will be primary method after consolidation) - $updateSettings: ⚠️ DEPRECATED (will be removed in favor of $saveSettings) - Future: Consolidate to single $saveSettings(changes, did?) method Files changed: - src/utils/PlatformServiceMixin.ts: Add conversion helper, fix save methods, add deprecation notices - src/views/SearchAreaView.vue: Remove manual JSON conversion - src/test/PlatformServiceMixinTest.vue: Add comprehensive test UI with highlighting - docs/migration-templates/updateSettings-consolidation-plan.md: Document future consolidation strategy --- .../updateSettings-consolidation-plan.md | 113 ++++++++++ src/test/PlatformServiceMixinTest.vue | 201 +++++++++++++++++- src/utils/PlatformServiceMixin.ts | 51 ++++- src/views/SearchAreaView.vue | 7 +- 4 files changed, 359 insertions(+), 13 deletions(-) create mode 100644 docs/migration-templates/updateSettings-consolidation-plan.md diff --git a/docs/migration-templates/updateSettings-consolidation-plan.md b/docs/migration-templates/updateSettings-consolidation-plan.md new file mode 100644 index 00000000..ab3780b0 --- /dev/null +++ b/docs/migration-templates/updateSettings-consolidation-plan.md @@ -0,0 +1,113 @@ +# $updateSettings to $saveSettings Consolidation Plan + +## Overview +Consolidate `$updateSettings` method into `$saveSettings` to eliminate code duplication and improve maintainability. The `$updateSettings` method is currently just a thin wrapper around `$saveSettings` and `$saveUserSettings`, providing no additional functionality. + +## Current State Analysis + +### Current Implementation +```typescript +// Current $updateSettings - just a wrapper +async $updateSettings(changes: Partial, did?: string): Promise { + try { + if (did) { + return await this.$saveUserSettings(did, changes); + } else { + return await this.$saveSettings(changes); + } + } catch (error) { + logger.error("[PlatformServiceMixin] Error updating settings:", error); + return false; + } +} +``` + +### Usage Statistics +- **$updateSettings**: 42 references across codebase +- **$saveSettings**: 38 references across codebase +- **$saveUserSettings**: 12 references across codebase + +## Migration Strategy + +### Phase 1: Documentation and Planning ✅ +- [x] Document current usage patterns +- [x] Identify all call sites +- [x] Create migration plan + +### Phase 2: Implementation +- [ ] Update `$saveSettings` to accept optional `did` parameter +- [ ] Add error handling to `$saveSettings` (currently missing) +- [ ] Deprecate `$updateSettings` with migration notice +- [ ] Update all call sites to use `$saveSettings` directly + +### Phase 3: Cleanup +- [ ] Remove `$updateSettings` method +- [ ] Update documentation +- [ ] Update tests + +## Implementation Details + +### Enhanced $saveSettings Method +```typescript +async $saveSettings(changes: Partial, did?: string): Promise { + try { + // Convert settings for database storage + const convertedChanges = this._convertSettingsForStorage(changes); + + if (did) { + // User-specific settings + return await this.$saveUserSettings(did, convertedChanges); + } else { + // Default settings + return await this.$saveSettings(convertedChanges); + } + } catch (error) { + logger.error("[PlatformServiceMixin] Error saving settings:", error); + return false; + } +} +``` + +### Migration Benefits +1. **Reduced Code Duplication**: Single method handles both use cases +2. **Improved Maintainability**: One place to fix issues +3. **Consistent Error Handling**: Unified error handling approach +4. **Better Type Safety**: Single method signature to maintain + +### Risk Assessment +- **Low Risk**: `$updateSettings` is just a wrapper, no complex logic +- **Backward Compatible**: Can maintain both methods during transition +- **Testable**: Existing tests can be updated incrementally + +## Call Site Migration Examples + +### Before (using $updateSettings) +```typescript +await this.$updateSettings({ searchBoxes: [newSearchBox] }); +await this.$updateSettings({ filterFeedByNearby: false }, userDid); +``` + +### After (using $saveSettings) +```typescript +await this.$saveSettings({ searchBoxes: [newSearchBox] }); +await this.$saveSettings({ filterFeedByNearby: false }, userDid); +``` + +## Testing Strategy +1. **Unit Tests**: Update existing tests to use `$saveSettings` +2. **Integration Tests**: Verify both default and user-specific settings work +3. **Migration Tests**: Ensure searchBoxes conversion still works +4. **Performance Tests**: Verify no performance regression + +## Timeline +- **Phase 1**: ✅ Complete +- **Phase 2**: 1-2 days +- **Phase 3**: 1 day +- **Total**: 2-3 days + +## Success Criteria +- [ ] All existing functionality preserved +- [ ] No performance regression +- [ ] All tests passing +- [ ] Reduced code duplication +- [ ] Improved maintainability \ No newline at end of file diff --git a/src/test/PlatformServiceMixinTest.vue b/src/test/PlatformServiceMixinTest.vue index 1ec225fa..3f635f71 100644 --- a/src/test/PlatformServiceMixinTest.vue +++ b/src/test/PlatformServiceMixinTest.vue @@ -1,11 +1,63 @@