Compare commits

...

16 Commits
1.0.2 ... 1.0.4

Author SHA1 Message Date
002f240720 bump to version 1.0.4 and build 37 2025-07-20 20:37:26 -06:00
ffe8d90161 fix: linting 2025-07-20 19:55:37 -06:00
6d6816d1a8 Merge pull request 'Deep-link fixes' (#145) from deep-link into master
Reviewed-on: #145
2025-07-15 02:49:12 -04:00
c1477d0266 Merge branch 'master' into deep-link 2025-07-14 23:42:21 -04:00
33ce6bdb72 fix: invite-one-accept deep link would not route properly 2025-07-14 20:49:40 -06:00
dc21e8dac3 bump version number and add '-beta' 2025-07-12 22:10:53 -06:00
a9a8ba217c bump to version 1.0.3 build 36 2025-07-12 22:10:07 -06:00
b0d99e7c1e fix: quick-and-dirty fix to get the correct environment variables 2025-07-12 20:17:38 -06:00
a96cc8155c fix incorrect checks for success 2025-07-04 16:58:18 -06:00
861408c7bc Consolidate deep-link paths to be derived from the same source so they don't get out of sync any more. 2025-07-03 17:01:08 -06:00
1b283a0045 Merge pull request 'Lock to Portrait Mode (iOS and Android)' (#141) from app-portrait-mode-lock into master
Reviewed-on: #141
2025-06-27 21:47:11 -04:00
afd407e178 add portrait-mode camera to CHANGELOG 2025-06-27 19:46:30 -06:00
Jose Olarte III
59b13823c8 Feature: lock orientation mode 2025-06-23 17:39:21 +08:00
3baa6633a6 on mobile: bump version to 1.0.2 and build to 35 2025-06-20 20:27:16 -06:00
bda98eb632 reword the account-download button 2025-06-20 19:36:16 -06:00
eea1cb995a bump to version 1.0.3-beta 2025-06-20 19:27:07 -06:00
18 changed files with 193 additions and 140 deletions

View File

@@ -61,15 +61,17 @@ Install dependencies:
* `npx prettier --write ./sw_scripts/` * `npx prettier --write ./sw_scripts/`
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`. * Update the ClickUp tasks & CHANGELOG.md & the version in package.json.
* Run install & build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build:web`
* If also deploying to mobile, update the new version in the ios & android filed.
* Commit everything (since the commit hash is used the app). * Commit everything (since the commit hash is used the app).
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
* Put the commit hash in the changelog (which will help you remember to bump the version in the step later). * Put the commit hash in the changelog (which will help you remember to bump the version in the step later).
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.0 && git push origin 1.0.0`. * Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.2 && git push origin 1.0.2`.
* For test, build the app (because test server is not yet set up to build): * For test, build the app (because test server is not yet set up to build):
@@ -93,11 +95,11 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* `pkgx +npm sh` * `pkgx +npm sh`
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.0 && npm install && npm run build:web && cd -` * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web && cd -`
(The plain `npm run build:web` uses the .env.production file.) (The plain `npm run build:web` uses the .env.production file.)
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-1 && mv crowd-funder-for-time-pwa/dist time-safari/` * Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-2 && mv crowd-funder-for-time-pwa/dist time-safari/`
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production. * Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production.
@@ -362,12 +364,9 @@ Prerequisites: macOS with Xcode installed
4. Bump the version to match Android & package.json: 4. Bump the version to match Android & package.json:
``` ```
cd ios/App cd ios/App && xcrun agvtool new-version 37 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.4;/g" App.xcodeproj/project.pbxproj && cd -
xcrun agvtool new-version 34
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj
cd -
``` ```
5. Open the project in Xcode: 5. Open the project in Xcode:
@@ -388,11 +387,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. * 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`). * 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 * 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 "-". * 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. * 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. * You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review". * Then "Save" and "Add to Review" and "Resubmit to App Review".
* Eventually it'll be "Ready for Distribution" which means
### Android Build ### Android Build

View File

@@ -6,7 +6,20 @@ 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.2] - 2025.06.20 ## [1.0.4] - 2025.07.20
### Fixed
- Deep link for invite-one-accept
## [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
### Added ### Added
- Version on feed title - Version on feed title

View File

@@ -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. 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 ## Other

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 34 versionCode 37
versionName "0.5.8" versionName "1.0.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -13,6 +13,7 @@
android:exported="true" android:exported="true"
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBarLaunch"> android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -403,7 +403,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.8; MARKETING_VERSION = 1.0.4;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -430,7 +430,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 34; CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.8; MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -37,8 +37,6 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.2", "version": "1.0.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.0.2", "version": "1.0.4",
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
"@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor-mlkit/barcode-scanning": "^6.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.0.2", "version": "1.0.4",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"

View File

@@ -27,37 +27,8 @@
*/ */
import { z } from "zod"; 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 // Parameter validation schemas for each route type
export const deepLinkSchemas = { export const deepLinkSchemas = {
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
claim: z.object({ claim: z.object({
id: z.string(), id: z.string(),
}), }),
@@ -72,16 +43,24 @@ export const deepLinkSchemas = {
"confirm-gift": z.object({ "confirm-gift": z.object({
id: z.string(), id: z.string(),
}), }),
"contact-edit": z.object({
did: z.string(),
}),
"contact-import": z.object({ "contact-import": z.object({
jwt: z.string(), jwt: z.string(),
}), }),
contacts: z.object({
contactJwt: z.string().optional(),
inviteJwt: z.string().optional(),
}),
did: z.object({ did: z.object({
did: z.string(), did: z.string(),
}), }),
"invite-one-accept": z.object({ "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-setup": z.object({ "onboard-meeting-members": z.object({
id: z.string(), id: z.string(),
}), }),
project: z.object({ project: z.object({
@@ -92,6 +71,21 @@ 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 = { export type DeepLinkParams = {
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>; [K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
}; };
@@ -100,3 +94,8 @@ export interface DeepLinkError extends Error {
code: string; code: string;
details?: unknown; details?: unknown;
} }
// Use the type to ensure route validation
export const routeSchema = z.enum(
VALID_DEEP_LINK_ROUTES as [string, ...string[]],
);

View File

@@ -72,12 +72,12 @@ const handleDeepLink = async (data: { url: string }) => {
await deepLinkHandler.handleDeepLink(data.url); await deepLinkHandler.handleDeepLink(data.url);
} catch (error) { } catch (error) {
logger.error("[DeepLink] Error handling deep link: ", error); logger.error("[DeepLink] Error handling deep link: ", error);
handleApiError( let message: string =
{ error instanceof Error ? error.message : safeStringify(error);
message: error instanceof Error ? error.message : safeStringify(error), if (data.url) {
} as AxiosError, message += `\nURL: ${data.url}`;
"deep-link", }
); handleApiError({ message } as AxiosError, "deep-link");
} }
}; };

View File

@@ -73,6 +73,11 @@ const routes: Array<RouteRecordRaw> = [
name: "contacts", name: "contacts",
component: () => import("../views/ContactsView.vue"), component: () => import("../views/ContactsView.vue"),
}, },
{
path: "/database-migration",
name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"),
},
{ {
path: "/did/:did?", path: "/did/:did?",
name: "did", name: "did",
@@ -139,8 +144,9 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("../views/InviteOneView.vue"), 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?", path: "/invite-one-accept/:jwt?",
name: "InviteOneAcceptView", name: "invite-one-accept",
component: () => import("../views/InviteOneAcceptView.vue"), component: () => import("../views/InviteOneAcceptView.vue"),
}, },
{ {
@@ -148,11 +154,6 @@ const routes: Array<RouteRecordRaw> = [
name: "logs", name: "logs",
component: () => import("../views/LogView.vue"), component: () => import("../views/LogView.vue"),
}, },
{
path: "/database-migration",
name: "database-migration",
component: () => import("../views/DatabaseMigration.vue"),
},
{ {
path: "/new-activity", path: "/new-activity",
name: "new-activity", name: "new-activity",

View File

@@ -44,6 +44,8 @@
*/ */
import { Router } from "vue-router"; import { Router } from "vue-router";
import { z } from "zod";
import { import {
deepLinkSchemas, deepLinkSchemas,
baseUrlSchema, baseUrlSchema,
@@ -53,6 +55,38 @@ import {
import { logConsoleAndDb } from "../db/databaseUtil"; import { logConsoleAndDb } from "../db/databaseUtil";
import type { DeepLinkError } from "../interfaces/deepLinks"; import type { DeepLinkError } from "../interfaces/deepLinks";
// Helper function to extract the first key from a Zod object schema
function getFirstKeyFromZodObject(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
schema: z.ZodObject<any>,
): 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<string, { name: string; paramKey?: string }> =
Object.entries(deepLinkSchemas).reduce(
(acc, [routeName, schema]) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
acc[routeName] = {
name: routeName,
paramKey,
};
return acc;
},
{} as Record<string, { name: string; paramKey?: string }>,
);
/** /**
* Handles processing and routing of deep links in the application. * Handles processing and routing of deep links in the application.
* Provides validation, error handling, and routing for deep link URLs. * Provides validation, error handling, and routing for deep link URLs.
@@ -69,30 +103,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. * Parses deep link URL into path, params and query components.
* Validates URL structure using Zod schemas. * Validates URL structure using Zod schemas.
* *
@@ -115,18 +126,9 @@ export class DeepLinkHandler {
const [path, queryString] = parts[1].split("?"); const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/"); const [routePath, ...pathParams] = path.split("/");
// logger.info(
// "[DeepLink] Debug:",
// "Route Path:",
// routePath,
// "Path Params:",
// pathParams,
// "Query String:",
// queryString,
// );
// Validate route exists before proceeding // Validate route exists before proceeding
if (!this.ROUTE_MAP[routePath]) { if (!ROUTE_MAP[routePath]) {
throw { throw {
code: "INVALID_ROUTE", code: "INVALID_ROUTE",
message: `Invalid route path: ${routePath}`, message: `Invalid route path: ${routePath}`,
@@ -144,9 +146,14 @@ export class DeepLinkHandler {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (pathParams) { if (pathParams) {
// Now we know routePath exists in ROUTE_MAP // 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("/"); 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 }; return { path: routePath, params, query };
} }
@@ -170,7 +177,7 @@ export class DeepLinkHandler {
try { try {
// Validate route exists // Validate route exists
const validRoute = routeSchema.parse(path) as DeepLinkRoute; const validRoute = routeSchema.parse(path) as DeepLinkRoute;
routeName = this.ROUTE_MAP[validRoute].name; routeName = ROUTE_MAP[validRoute].name;
} catch (error) { } catch (error) {
// Log the invalid route attempt // Log the invalid route attempt
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true); logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
@@ -178,51 +185,69 @@ export class DeepLinkHandler {
// Redirect to error page with information about the invalid link // Redirect to error page with information about the invalid link
await this.router.replace({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "INVALID_ROUTE", errorCode: "INVALID_ROUTE",
message: `The link you followed (${path}) is not supported`, errorMessage: `The link you followed (${path}) is not supported`,
...query,
}, },
}); });
throw { // This previously threw an error but we're redirecting so there's no need.
code: "INVALID_ROUTE", return;
message: `Unsupported route: ${path}`,
};
} }
// Continue with parameter validation as before... // Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas]; const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
let validatedParams, validatedQuery;
try { try {
const validatedParams = await schema.parseAsync({ validatedParams = await schema.parseAsync(params);
...params, validatedQuery = await schema.parseAsync(query);
...query,
});
await this.router.replace({
name: routeName,
params: validatedParams,
query,
});
} catch (error) { } catch (error) {
// For parameter validation errors, provide specific error feedback // 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({ await this.router.replace({
name: "deep-link-error", name: "deep-link-error",
params,
query: { query: {
originalPath: path, originalPath: path,
errorCode: "INVALID_PARAMETERS", errorCode: "INVALID_PARAMETERS",
message: `The link parameters are invalid: ${(error as Error).message}`, errorMessage: `The link parameters are invalid: ${(error as Error).message}`,
...query,
}, },
}); });
throw { // This previously threw an error but we're redirecting so there's no need.
code: "INVALID_PARAMETERS", return;
message: (error as Error).message, }
details: error,
params: params, try {
query: query, await this.router.replace({
}; name: routeName,
params: validatedParams,
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: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
...validatedQuery,
},
});
} }
} }
@@ -235,7 +260,6 @@ export class DeepLinkHandler {
*/ */
async handleDeepLink(url: string): Promise<void> { async handleDeepLink(url: string): Promise<void> {
try { try {
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
const { path, params, query } = this.parseDeepLink(url); const { path, params, query } = this.parseDeepLink(url);
// Ensure params is always a Record<string,string> by converting undefined to empty string // Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries( const sanitizedParams = Object.fromEntries(
@@ -245,7 +269,7 @@ export class DeepLinkHandler {
} catch (error) { } catch (error) {
const deepLinkError = error as DeepLinkError; const deepLinkError = error as DeepLinkError;
logConsoleAndDb( logConsoleAndDb(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`, `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
true, true,
); );

View File

@@ -102,7 +102,7 @@
icon-name="chart" icon-name="chart"
svg-class="-ml-1 mr-3 h-5 w-5" svg-class="-ml-1 mr-3 h-5 w-5"
/> />
Download Account Show Account Seed
</button> </button>
<button <button

View File

@@ -31,7 +31,11 @@
<h2>Supported Deep Links</h2> <h2>Supported Deep Links</h2>
<ul> <ul>
<li v-for="(routeItem, index) in validRoutes" :key="index"> <li v-for="(routeItem, index) in validRoutes" :key="index">
<code>timesafari://{{ routeItem }}/:id</code> <code
>timesafari://{{ routeItem }}/:{{
deepLinkSchemaKeys[routeItem]
}}</code
>
</li> </li>
</ul> </ul>
</div> </div>
@@ -41,12 +45,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from "vue"; import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { VALID_DEEP_LINK_ROUTES } from "../interfaces/deepLinks"; import {
VALID_DEEP_LINK_ROUTES,
deepLinkSchemas,
} from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil"; import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
// an object with the route as the key and the first param name as the value
const deepLinkSchemaKeys = Object.fromEntries(
Object.entries(deepLinkSchemas).map(([route, schema]) => {
const param = Object.keys(schema.shape)[0];
return [route, param];
}),
);
// Extract error information from query params // Extract error information from query params
const errorCode = computed( const errorCode = computed(
@@ -54,7 +68,7 @@ const errorCode = computed(
); );
const errorMessage = computed( const errorMessage = computed(
() => () =>
(route.query.message as string) || (route.query.errorMessage as string) ||
"The deep link you followed is invalid or not supported.", "The deep link you followed is invalid or not supported.",
); );
const originalPath = computed(() => route.query.originalPath as string); const originalPath = computed(() => route.query.originalPath as string);
@@ -93,7 +107,7 @@ const reportIssue = () => {
// Log the error for analytics // Log the error for analytics
onMounted(() => { onMounted(() => {
logConsoleAndDb( logConsoleAndDb(
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}`, `[DeepLinkError] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`,
true, true,
); );
}); });

View File

@@ -128,7 +128,10 @@ export default class InviteOneAcceptView extends Vue {
} }
// Extract JWT from route path // Extract JWT from route path
const jwt = (this.$route.params.jwt as string) || ""; const jwt =
(this.$route.params.jwt as string) ||
(this.$route.query.jwt as string) ||
"";
await this.processInvite(jwt, false); await this.processInvite(jwt, false);
this.checkingInvite = false; this.checkingInvite = false;

View File

@@ -144,7 +144,7 @@ export default class QuickActionBvcBeginView extends Vue {
"HUR", "HUR",
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
); );
if (timeResult.type === "success") { if (timeResult.success) {
timeSuccess = true; timeSuccess = true;
} else { } else {
logger.error("Error sending time:", timeResult); logger.error("Error sending time:", timeResult);
@@ -153,9 +153,7 @@ export default class QuickActionBvcBeginView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text: timeResult?.error || "There was an error sending the time.",
timeResult?.error?.userMessage ||
"There was an error sending the time.",
}, },
5000, 5000,
); );
@@ -171,7 +169,7 @@ export default class QuickActionBvcBeginView extends Vue {
apiServer, apiServer,
axios, axios,
); );
if (attendResult.type === "success") { if (attendResult.success) {
attendedSuccess = true; attendedSuccess = true;
} else { } else {
logger.error("Error sending attendance:", attendResult); logger.error("Error sending attendance:", attendResult);
@@ -181,7 +179,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
attendResult?.error?.userMessage || attendResult?.error ||
"There was an error sending the attendance.", "There was an error sending the attendance.",
}, },
5000, 5000,

View File

@@ -6,7 +6,9 @@ import path from "path";
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
// Load environment variables // 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 __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);