From 861408c7bc38fa16d17658d60e3024f2b7b7593f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 3 Jul 2025 17:01:08 -0600 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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 @@