Compare commits

..

5 Commits

15 changed files with 101 additions and 54 deletions

View File

@@ -61,9 +61,11 @@ Install dependencies:
* `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 a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
* 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).
@@ -362,7 +364,7 @@ Prerequisites: macOS with Xcode installed
4. Bump the version to match Android & package.json:
```
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 -
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 -
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
```

View File

@@ -6,7 +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).
## [1.0.3] - 2025.07.12
## [1.0.4] - 2025.07.20 - 002f2407208d56cc59c0aa7c880535ae4cbace8b
### Fixed
- Deep link for invite-one-accept
## [1.0.3] - 2025.07.12 - a9a8ba217cd6015321911e98e6843e988dc2c4ae
### Changed
- Photo is pinned to profile mode
### Fixed

View File

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

View File

@@ -403,7 +403,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 36;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
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 = 36;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.3;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

4
package-lock.json generated
View File

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

View File

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

View File

@@ -60,9 +60,11 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import * as R from "ramda";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { Contact, ContactMaybeWithJsonStrings, ContactMethod } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger";
@@ -72,6 +74,7 @@ import {
PlatformCapabilities,
} from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
import { parseJsonField } from "../db/databaseUtil";
/**
* @vue-component
@@ -133,13 +136,13 @@ export default class DataExportSection extends Vue {
*/
public async exportDatabase() {
try {
let allContacts: Contact[] = [];
let allDbContacts: ContactMaybeWithJsonStrings[] = [];
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
allContacts = databaseUtil.mapQueryResultToValues(
allDbContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
) as unknown as ContactMaybeWithJsonStrings[];
}
// if (USE_DEXIE_DB) {
// await db.open();
@@ -147,6 +150,19 @@ export default class DataExportSection extends Vue {
// }
// Convert contacts to export format
const allContacts: Contact[] = allDbContacts.map((contact) => {
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
const exContact: Contact = R.omit(
["contactMethods"],
contact,
);
// now add contactMethods as a true array of ContactMethod objects
exContact.contactMethods = contact.contactMethods
? parseJsonField(contact.contactMethods, [] as Array<ContactMethod>)
: undefined;
return exContact;
});
const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });

View File

@@ -30,6 +30,17 @@ export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string;
};
/**
* This is for those cases (eg. with a DB) where field values may be all primitives or may be JSON values.
* See src/db/databaseUtil.ts parseJsonField for more details.
*
* This is so that we can reuse most of the type and don't have to maintain another copy.
* Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
*/
export type ContactMaybeWithJsonStrings = Omit<Contact, "contactMethods"> & {
contactMethods?: string | Array<ContactMethod>;
};
export const ContactSchema = {
contacts: "&did, name", // no need to key by other things
};

View File

@@ -82,7 +82,9 @@ export const baseUrlSchema = z.object({
});
// Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = Object.keys(deepLinkSchemas) as readonly (keyof typeof deepLinkSchemas)[];
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]>;
@@ -94,4 +96,6 @@ export interface DeepLinkError extends Error {
}
// Use the type to ensure route validation
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES as [string, ...string[]]);
export const routeSchema = z.enum(
VALID_DEEP_LINK_ROUTES as [string, ...string[]],
);

View File

@@ -17,7 +17,7 @@ import {
updateDefaultSettings,
} from "../db/index";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
import {
@@ -966,31 +966,19 @@ export interface DatabaseExport {
}
/**
* Converts an array of contacts to the standardized database export JSON format.
* Converts an array of contacts to the export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert
* @returns DatabaseExport object in the standardized format
*/
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included
const rows = contacts.map((contact) => {
const exContact: ContactWithJsonStrings = R.omit(
["contactMethods"],
contact,
);
exContact.contactMethods = contact.contactMethods
? JSON.stringify(contact.contactMethods, [])
: undefined;
return exContact;
});
return {
data: {
data: [
{
tableName: "contacts",
rows,
rows: contacts,
},
],
},

View File

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

View File

@@ -56,7 +56,10 @@ 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<any>): string | undefined {
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;
@@ -71,14 +74,18 @@ function getFirstKeyFromZodObject(schema: z.ZodObject<any>): string | undefined
* 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]) => {
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
paramKey,
};
return acc;
}, {} as Record<string, { name: string; paramKey?: string }>);
},
{} as Record<string, { name: string; paramKey?: string }>,
);
/**
* Handles processing and routing of deep links in the application.
@@ -200,7 +207,10 @@ export class DeepLinkHandler {
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);
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,
@@ -223,7 +233,10 @@ export class DeepLinkHandler {
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);
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",
@@ -231,12 +244,11 @@ export class DeepLinkHandler {
query: {
originalPath: path,
errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${(JSON.stringify(error))}`,
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
...validatedQuery,
},
});
}
}
/**

View File

@@ -31,7 +31,11 @@
<h2>Supported Deep Links</h2>
<ul>
<li v-for="(routeItem, index) in validRoutes" :key="index">
<code>timesafari://{{ routeItem }}/:{{ deepLinkSchemaKeys[routeItem] }}</code>
<code
>timesafari://{{ routeItem }}/:{{
deepLinkSchemaKeys[routeItem]
}}</code
>
</li>
</ul>
</div>
@@ -41,7 +45,10 @@
<script setup lang="ts">
import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { VALID_DEEP_LINK_ROUTES, deepLinkSchemas } from "../interfaces/deepLinks";
import {
VALID_DEEP_LINK_ROUTES,
deepLinkSchemas,
} from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger";
@@ -52,7 +59,7 @@ 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

View File

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

View File

@@ -153,9 +153,7 @@ export default class QuickActionBvcBeginView extends Vue {
group: "alert",
type: "danger",
title: "Error",
text:
timeResult?.error ||
"There was an error sending the time.",
text: timeResult?.error || "There was an error sending the time.",
},
5000,
);