Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 002f240720 | |||
| ffe8d90161 | |||
| 6d6816d1a8 | |||
| c1477d0266 | |||
| 33ce6bdb72 | |||
| dc21e8dac3 | |||
| a9a8ba217c | |||
| b0d99e7c1e | |||
| a96cc8155c | |||
| 861408c7bc | |||
| 1b283a0045 | |||
| afd407e178 | |||
|
|
59b13823c8 | ||
| 3baa6633a6 | |||
| bda98eb632 | |||
| eea1cb995a |
22
BUILDING.md
22
BUILDING.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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 = "";
|
||||||
|
|||||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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[]],
|
||||||
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user