Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Raymer
3881144c62 feat: add missing settings management functions
- Add getSettingsForAccount() to retrieve complete settings for any account DID
- Add getAccountSpecificSettings() to get account-specific settings without defaults
- Add mergeSettings() for consistent settings merging across SQLite and Dexie
- Update retrieveSettingsForActiveAccount() to use new mergeSettings() function
- Improve error handling and logging for settings operations
- Maintain backward compatibility with existing settings API

This provides better settings management capabilities for account switching,
debugging, and data validation while ensuring consistent behavior across
both SQLite and Dexie storage implementations.
2025-06-10 12:44:52 +00:00
42 changed files with 983 additions and 1216 deletions

1
.gitignore vendored
View File

@@ -55,4 +55,3 @@ build_logs/
icons
android/app/src/main/res/

View File

@@ -321,11 +321,11 @@ Prerequisites: macOS with Xcode installed
#### Each Release
0. First time (or if dependencies change):
0. First time (or if XCode dependencies change):
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx:
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
@@ -334,9 +334,12 @@ Prerequisites: macOS with Xcode installed
export GEM_PATH=$shortened_path
```
1. Check the iOS flag isIOS in CapacitorPlatformService (currently hard-coded for iOS build).
```bash
cd ios/App
pod install
```
2. Build the web assets:
1. Build the web assets:
```bash
rm -rf dist
@@ -344,7 +347,8 @@ Prerequisites: macOS with Xcode installed
npm run build:capacitor
```
3. Update iOS project with latest build:
2. Update iOS project with latest build:
```bash
npx cap sync ios
@@ -352,7 +356,7 @@ Prerequisites: macOS with Xcode installed
- If that fails with "Could not find..." then look at the "gem_path" instructions above.
4. Copy the assets:
3. Copy the assets:
```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
@@ -363,14 +367,15 @@ Prerequisites: macOS with Xcode installed
npx capacitor-assets generate --ios
```
4. Bump the version to match Android & package.json:
4. Bump the version to match Android:
```
cd ios/App
xcrun agvtool new-version 30
xcrun agvtool new-version 25
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.4;/g" > temp && mv temp App.xcodeproj/project.pbxproj
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.0;/g" > temp
mv temp App.xcodeproj/project.pbxproj
cd -
```
@@ -398,8 +403,6 @@ Prerequisites: macOS with Xcode installed
* You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review".
8. Revert the iOS flag isIOS in CapacitorPlatformService.
### Android Build
Prerequisites: Android Studio with Java SDK installed
@@ -424,7 +427,7 @@ Prerequisites: Android Studio with Java SDK installed
npx capacitor-assets generate --android
```
4. Bump version to match iOS & package.json: android/app/build.gradle
4. Bump version to match iOS: android/app/build.gradle
5. Open the project in Android Studio:
@@ -475,7 +478,7 @@ At play.google.com/console:
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
## Android Configuration for deep links
## First-time Android Configuration for deep links
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
@@ -486,6 +489,4 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="timesafari" />
</intent-filter>
```
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
```

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 30
versionName "0.5.4"
versionCode 25
versionName "0.5.0"
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 = 30;
CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.5.4;
MARKETING_VERSION = 0.5.0;
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 = 30;
CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.5.4;
MARKETING_VERSION = 0.5.0;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -49,16 +49,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>app.timesafari</string>
<key>CFBundleURLSchemes</key>
<array>
<string>timesafari</string>
</array>
</dict>
</array>
</dict>
</plist>

1215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -104,6 +104,7 @@ import { USE_DEXIE_DB } from "@/constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: {
@@ -142,23 +143,19 @@ export default class FeedFilters extends Vue {
async toggleHasVisibleDid() {
this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid;
await databaseUtil.updateDefaultSettings({
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
}
async toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
await databaseUtil.updateDefaultSettings({
filterFeedByNearby: this.isNearby,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
[this.isNearby, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -172,10 +169,11 @@ export default class FeedFilters extends Vue {
this.settingChanged = true;
}
await databaseUtil.updateDefaultSettings({
filterFeedByNearby: false,
filterFeedByVisible: false,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
[false, false, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -193,10 +191,11 @@ export default class FeedFilters extends Vue {
this.settingChanged = true;
}
await databaseUtil.updateDefaultSettings({
filterFeedByNearby: true,
filterFeedByVisible: true,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
[true, true, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {

View File

@@ -321,7 +321,7 @@ export default class GiftedDialog extends Vue {
);
if (!result.success) {
const errorMessage = result.error;
const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result);
this.$notify(
{
@@ -367,6 +367,19 @@ export default class GiftedDialog extends Vue {
// Helper functions for readability
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
explainData() {
this.$notify(
{

View File

@@ -227,7 +227,6 @@ export default class GivenPrompts extends Vue {
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
let count = 0;
// as long as the index has an entry, loop
while (
this.shownContactDbIndices[someContactDbIndex] != null &&
@@ -246,8 +245,9 @@ export default class GivenPrompts extends Vue {
[someContactDbIndex],
);
if (result) {
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
this.currentContact = mappedContacts[0] as unknown as Contact;
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
someContactDbIndex
] as unknown as Contact;
}
if (USE_DEXIE_DB) {
await db.open();

View File

@@ -48,15 +48,12 @@
<span>
{{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<router-link
:to="{ path: '/did/' + encodeURIComponent(visDid) }"
class="text-blue-500"
>
<a :href="`/did/${visDid}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</a>
</span>
</span>
</div>

View File

@@ -250,7 +250,7 @@ export default class OfferDialog extends Vue {
);
if (!result.success) {
const errorMessage = result.error;
const errorMessage = this.getOfferCreationErrorMessage(result);
logger.error("Error with offer creation result:", result);
this.$notify(
{
@@ -290,6 +290,21 @@ export default class OfferDialog extends Vue {
);
}
}
// Helper functions for readability
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getOfferCreationErrorMessage(result: any) {
return (
serverMessageForUser(result) ||
result.error?.userMessage ||
result.error?.error
);
}
}
</script>

View File

@@ -259,7 +259,7 @@ export default class OnboardingDialog extends Vue {
this.visible = true;
if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
if (USE_DEXIE_DB) {
@@ -273,7 +273,7 @@ export default class OnboardingDialog extends Vue {
async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false;
if (done) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
if (USE_DEXIE_DB) {

View File

@@ -38,14 +38,14 @@ export default class TopMessage extends Vue {
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message = "You're not using prod, user " + didPrefix;
this.message = "You're linked to a non-prod server, user " + didPrefix;
} else if (
settings.warnIfProdServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
this.message =
"You are using prod, user " + didPrefix;
"You're linked to the production server, user " + didPrefix;
}
} catch (err: unknown) {
this.$notify(

View File

@@ -37,20 +37,7 @@ export async function updateDefaultSettings(
}
}
export async function insertDidSpecificSettings(
did: string,
settings: Partial<Settings> = {},
): Promise<boolean> {
const platform = PlatformServiceFactory.getInstance();
const { sql, params } = generateInsertStatement(
{ ...settings, accountDid: did }, // make sure accountDid is set to the given value
"settings",
);
const result = await platform.dbExec(sql, params);
return result.changes === 1;
}
export async function updateDidSpecificSettings(
export async function updateAccountSettings(
accountDid: string,
settingsChanges: Settings,
): Promise<boolean> {
@@ -109,6 +96,9 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount();
// If no active DID, return defaults
if (!defaultSettings.activeDid) {
logConsoleAndDb(
"[databaseUtil] No active DID found, returning default settings",
);
return defaultSettings;
}
@@ -121,29 +111,20 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
);
if (!result?.values?.length) {
// we created DID-specific settings when generated or imported, so this shouldn't happen
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
);
return defaultSettings;
}
// Map and filter settings
// Map settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
);
// Merge settings
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
// Handle searchBoxes parsing
if (settings.searchBoxes) {
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
}
return settings;
// Use the new mergeSettings function for consistency
return mergeSettings(defaultSettings, overrideSettings);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
@@ -240,7 +221,6 @@ export function generateInsertStatement(
const values = Object.values(model).filter((value) => value !== undefined);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
return {
sql: insertSql,
params: values,
@@ -314,113 +294,126 @@ export function mapColumnsToValues(
}
/**
* Debug function to inspect raw settings data in the database
* This helps diagnose issues with data corruption or malformed JSON
* @param did Optional DID to inspect specific account settings
* @author Matthew Raymer
* Retrieves settings for a specific account by DID
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Combined settings for the specified account
*/
export async function debugSettingsData(did?: string): Promise<void> {
export async function getSettingsForAccount(accountDid: string): Promise<Settings> {
try {
// Get default settings first
const defaultSettings = await retrieveSettingsForDefaultAccount();
// Get account-specific settings
const platform = PlatformServiceFactory.getInstance();
// Get all settings records
const allSettings = await platform.dbQuery("SELECT * FROM settings");
logConsoleAndDb(
`[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`,
false,
const result = await platform.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[accountDid],
);
if (allSettings?.values?.length) {
allSettings.values.forEach((row, index) => {
const settings = mapColumnsToValues(allSettings.columns, [row])[0];
logConsoleAndDb(`[DEBUG] Settings record ${index + 1}:`, false);
logConsoleAndDb(`[DEBUG] - ID: ${settings.id}`, false);
logConsoleAndDb(`[DEBUG] - accountDid: ${settings.accountDid}`, false);
logConsoleAndDb(`[DEBUG] - activeDid: ${settings.activeDid}`, false);
if (settings.searchBoxes) {
logConsoleAndDb(
`[DEBUG] - searchBoxes type: ${typeof settings.searchBoxes}`,
false,
);
logConsoleAndDb(
`[DEBUG] - searchBoxes value: ${String(settings.searchBoxes)}`,
false,
);
// Try to parse it
try {
const parsed = JSON.parse(String(settings.searchBoxes));
logConsoleAndDb(
`[DEBUG] - searchBoxes parsed successfully: ${JSON.stringify(parsed)}`,
false,
);
} catch (parseError) {
logConsoleAndDb(
`[DEBUG] - searchBoxes parse error: ${parseError}`,
true,
);
}
}
logConsoleAndDb(
`[DEBUG] - Full record: ${JSON.stringify(settings, null, 2)}`,
false,
);
});
}
// If specific DID provided, also check accounts table
if (did) {
const account = await platform.dbQuery(
"SELECT * FROM accounts WHERE did = ?",
[did],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`,
false,
`[databaseUtil] No account-specific settings found for ${accountDid}`,
);
return defaultSettings;
}
// Map and merge settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
return mergeSettings(defaultSettings, overrideSettings);
} catch (error) {
logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true);
logConsoleAndDb(
`[databaseUtil] Failed to retrieve settings for account ${accountDid}: ${error}`,
true,
);
// Return default settings on error
return await retrieveSettingsForDefaultAccount();
}
}
/**
* Platform-agnostic JSON parsing utility
* Handles different SQLite implementations:
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* @param value The value to parse (could be string or already parsed object)
* @param defaultValue Default value if parsing fails
* @returns Parsed object or default value
* @author Matthew Raymer
* Retrieves only account-specific settings for a given DID (without merging with defaults)
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Account-specific settings only
*/
export function parseJsonField<T>(value: unknown, defaultValue: T): T {
export async function getAccountSpecificSettings(accountDid: string): Promise<Settings> {
try {
// If already an object (web SQLite auto-parsed), return as-is
if (typeof value === "object" && value !== null) {
return value as T;
const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[accountDid],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${accountDid}`,
);
return {};
}
// If it's a string (Capacitor SQLite or fallback), parse it
if (typeof value === "string") {
return JSON.parse(value) as T;
// Map settings
const settings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
// Handle searchBoxes parsing
if (settings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${accountDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
}
// If it's null/undefined, return default
if (value === null || value === undefined) {
return defaultValue;
}
return defaultValue;
return settings;
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse JSON field: ${error}`,
`[databaseUtil] Failed to retrieve account-specific settings for ${accountDid}: ${error}`,
true,
);
return defaultValue;
return {};
}
}
/**
* Merges default settings with account-specific settings using consistent logic
* @param defaultSettings - The default/master settings
* @param accountSettings - The account-specific settings to merge
* @returns Settings - Merged settings with account-specific overrides
*/
export function mergeSettings(defaultSettings: Settings, accountSettings: Settings): Settings {
// Filter out null values from account settings
const accountSettingsFiltered = Object.fromEntries(
Object.entries(accountSettings).filter(([_, v]) => v !== null),
);
// Perform shallow merge (account settings override defaults)
const mergedSettings = { ...defaultSettings, ...accountSettingsFiltered };
// Handle searchBoxes parsing if present
if (mergedSettings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
mergedSettings.searchBoxes = JSON.parse(mergedSettings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes during merge: ${error}`,
true,
);
// Reset to empty array on parse failure
mergedSettings.searchBoxes = [];
}
}
return mergedSettings;
}

View File

@@ -265,6 +265,43 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
}
}
/**
* Retrieves settings for a specific account by DID
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Combined settings for the specified account
*/
export async function getSettingsForAccount(accountDid: string): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount();
const overrideSettings =
(await db.settings
.where("accountDid")
.equals(accountDid)
.first()) || {};
return R.mergeDeepRight(defaultSettings, overrideSettings);
}
/**
* Retrieves only account-specific settings for a given DID (without merging with defaults)
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Account-specific settings only
*/
export async function getAccountSpecificSettings(accountDid: string): Promise<Settings> {
return (await db.settings
.where("accountDid")
.equals(accountDid)
.first()) || {};
}
/**
* Merges default settings with account-specific settings using consistent logic
* @param defaultSettings - The default/master settings
* @param accountSettings - The account-specific settings to merge
* @returns Settings - Merged settings with account-specific overrides
*/
export function mergeSettings(defaultSettings: Settings, accountSettings: Settings): Settings {
return R.mergeDeepRight(defaultSettings, accountSettings);
}
export async function updateAccountSettings(
accountDid: string,
settingsChanges: Settings,

View File

@@ -1,4 +1,6 @@
import { AxiosResponse } from "axios";
import { GiverReceiverInputInfo } from "../libs/util";
import { ErrorResult, ResultWithType } from "./common";
export interface GiverOutputInfo {
action: string;
@@ -45,3 +47,12 @@ export interface ProviderInfo {
*/
linkConfirmed: boolean;
}
// Type for createAndSubmitClaim result
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// Update SuccessResult to use ClaimResult
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}

View File

@@ -15,6 +15,10 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
publicUrls?: Record<string, string>;
}
export interface ResultWithType {
type: string;
}
export interface ErrorResponse {
error?: {
message?: string;
@@ -26,6 +30,11 @@ export interface InternalError {
userMessage?: string;
}
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export interface KeyMeta {
did: string;
publicKeyHex: string;

View File

@@ -30,7 +30,7 @@ import { z } from "zod";
// Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = [
"user-profile",
"project",
"project-details",
"onboard-meeting-setup",
"invite-one-accept",
"contact-import",
@@ -61,7 +61,7 @@ export const deepLinkSchemas = {
"user-profile": z.object({
id: z.string(),
}),
"project": z.object({
"project-details": z.object({
id: z.string(),
}),
"onboard-meeting-setup": z.object({

View File

@@ -1,6 +1,5 @@
export type {
// From common.ts
CreateAndSubmitClaimResult,
GenericCredWrapper,
GenericVerifiableCredential,
KeyMeta,
@@ -19,6 +18,11 @@ export type {
RegisterActionClaim,
} from "./claims";
export type {
// From claims-result.ts
CreateAndSubmitClaimResult,
} from "./claims-result";
export type {
// From records.ts
PlanSummaryRecord,

View File

@@ -979,7 +979,7 @@ export const createAndSubmitConfirmation = async (
handleId: string | undefined,
apiServer: string,
axios: Axios,
): Promise<CreateAndSubmitClaimResult> => {
) => {
const goodClaim = removeSchemaContext(
removeVisibleToDids(
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),

View File

@@ -44,7 +44,6 @@ import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { sha256 } from "ethereum-cryptography/sha256";
import { IIdentifier } from "@veramo/core";
import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
export interface GiverReceiverInputInfo {
did?: string;
@@ -698,7 +697,6 @@ export async function saveNewIdentity(
];
await platformService.dbExec(sql, params);
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
await databaseUtil.insertDidSpecificSettings(identity.did);
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
@@ -712,7 +710,6 @@ export async function saveNewIdentity(
publicKeyHex: identity.keys[0].publicKeyHex,
});
await updateDefaultSettings({ activeDid: identity.did });
await insertDidSpecificSettings(identity.did);
}
} catch (error) {
logger.error("Failed to update default settings:", error);
@@ -735,9 +732,7 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
await saveNewIdentity(newId, mnemonic, derivationPath);
await databaseUtil.updateDidSpecificSettings(newId.did, {
isRegistered: false,
});
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
if (USE_DEXIE_DB) {
await updateAccountSettings(newId.did, { isRegistered: false });
}
@@ -779,7 +774,7 @@ export const registerSaveAndActivatePasskey = async (
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
await databaseUtil.updateDidSpecificSettings(account.did, {
await databaseUtil.updateAccountSettings(account.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) {
@@ -867,7 +862,7 @@ export const contactToCsvLine = (contact: Contact): string => {
// Handle contactMethods array by stringifying it
const contactMethodsStr = contact.contactMethods
? escapeField(JSON.stringify(parseJsonField(contact.contactMethods, [])))
? escapeField(JSON.stringify(contact.contactMethods))
: "";
const fields = [
@@ -912,7 +907,7 @@ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
did: contact.did,
name: contact.name || null,
contactMethods: contact.contactMethods
? JSON.stringify(parseJsonField(contact.contactMethods, []))
? JSON.stringify(contact.contactMethods)
: null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null,

View File

@@ -28,7 +28,7 @@
*
* Supported Routes:
* - user-profile: View user profile
* - project: View project details
* - project-details: View project details
* - onboard-meeting-setup: Setup onboarding meeting
* - invite-one-accept: Accept invitation
* - contact-import: Import contacts
@@ -81,16 +81,18 @@ export class DeepLinkHandler {
string,
{ name: string; paramKey?: string }
> = {
"claim": { name: "claim" },
"claim-add-raw": { name: "claim-add-raw" },
"claim-cert": { name: "claim-cert" },
"confirm-gift": { name: "confirm-gift" },
"did": { name: "did", paramKey: "did" },
"invite-one-accept": { name: "invite-one-accept" },
"onboard-meeting-members": { name: "onboard-meeting-members" },
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
"project": { name: "project" },
"user-profile": { name: "user-profile" },
"project-details": { name: "project-details" },
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
"invite-one-accept": { name: "invite-one-accept" },
"contact-import": { name: "contact-import" },
"confirm-gift": { name: "confirm-gift" },
claim: { name: "claim" },
"claim-cert": { name: "claim-cert" },
"claim-add-raw": { name: "claim-add-raw" },
"contact-edit": { name: "contact-edit", paramKey: "did" },
contacts: { name: "contacts" },
did: { name: "did", paramKey: "did" },
};
/**

View File

@@ -5,7 +5,6 @@ import {
CameraSource,
CameraDirection,
} from "@capacitor/camera";
import { Capacitor } from "@capacitor/core";
import { Share } from "@capacitor/share";
import {
SQLiteConnection,
@@ -248,7 +247,7 @@ export class CapacitorPlatformService implements PlatformService {
hasFileSystem: true,
hasCamera: true,
isMobile: true,
isIOS: Capacitor.getPlatform() === "ios",
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: false,
needsFileHandlingInstructions: true,
isNativeApp: true,

View File

@@ -1814,7 +1814,7 @@ export default class AccountViewView extends Vue {
if (!this.isRegistered) {
// the user was not known to be registered, but now they are (because we got no error) so let's record it
try {
await databaseUtil.updateDidSpecificSettings(did, {
await databaseUtil.updateAccountSettings(did, {
isRegistered: true,
});
if (USE_DEXIE_DB) {
@@ -2018,7 +2018,7 @@ export default class AccountViewView extends Vue {
if ((error as any).response.status === 404) {
logger.error("The image was already deleted:", error);
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
profileImageUrl: undefined,
});
if (USE_DEXIE_DB) {

View File

@@ -198,7 +198,7 @@ export default class ClaimAddRawView extends Vue {
this.apiServer,
this.axios,
);
if (result.success) {
if (result.type === "success") {
this.$notify(
{
group: "alert",

View File

@@ -46,7 +46,6 @@
</h2>
<div class="flex justify-center w-full">
<router-link
v-if="veriClaim.id"
:to="'/claim-cert/' + encodeURIComponent(veriClaim.id)"
class="text-blue-500 mt-2"
title="Printable Certificate"
@@ -293,17 +292,12 @@
<div class="text-sm">
{{ didInfo(confirmerId) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
<router-link
:to="{
path: '/did/' + encodeURIComponent(confirmerId),
}"
class="text-blue-500"
>
<a :href="`/did/${confirmerId}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</a>
</span>
</div>
</div>
@@ -335,17 +329,12 @@
<div class="text-sm">
{{ didInfo(confsVisibleTo) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
<router-link
:to="{
path: '/did/' + encodeURIComponent(confsVisibleTo),
}"
class="text-blue-500"
>
<a :href="`/did/${confsVisibleTo}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</a>
</span>
</div>
</div>
@@ -454,17 +443,12 @@
<span>
{{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<router-link
:to="{
path: '/did/' + encodeURIComponent(visDid),
}"
class="text-blue-500"
>
<a :href="`/did/${visDid}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</a>
</span>
<span v-if="veriClaim.publicUrls?.[visDid]"
>, found at&nbsp;<a
@@ -941,7 +925,7 @@ export default class ClaimView extends Vue {
this.apiServer,
this.axios,
);
if (result.success) {
if (result.type === "success") {
this.$notify(
{
group: "alert",

View File

@@ -407,14 +407,14 @@
</a>
</div>
<div class="mt-2 ml-2">
<router-link
<a
v-if="isRegistered"
class="text-blue-500 cursor-pointer"
:to="urlForNewGive"
:href="urlForNewGive"
>
<font-awesome icon="file-lines" />
Record a Give Similar to the Original
</router-link>
</a>
</div>
</div>
</div>
@@ -831,7 +831,7 @@ export default class ConfirmGiftView extends Vue {
this.apiServer,
this.axios,
);
if (result.success) {
if (result.type === "success") {
this.$notify(
{
group: "alert",

View File

@@ -138,13 +138,11 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { Contact, ContactMethod } from "../db/tables/contacts";
import { AppString } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* Contact Edit View Component
@@ -232,7 +230,9 @@ export default class ContactEditView extends Vue {
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
dbContact,
)[0] as unknown as Contact;
contact.contactMethods = parseJsonField(contact?.contactMethods, []);
contact.contactMethods = JSON.parse(
(contact?.contactMethods as unknown as string) || "[]",
);
if (USE_DEXIE_DB) {
await db.open();
contact = await db.contacts.get(contactDid || "");

View File

@@ -213,7 +213,6 @@ import {
} from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import * as libsUtil from "../libs/util";
import {
capitalizeAndInsertSpacesBeforeCaps,
@@ -290,7 +289,7 @@ function dbRecordToContact(record: ContactDbRecord): Contact {
profileImageUrl: safeString(record.profileImageUrl),
publicKeyBase64: safeString(record.publicKeyBase64),
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
contactMethods: parseJsonField(record.contactMethods, []),
contactMethods: JSON.parse(record.contactMethods || "[]"),
};
}

View File

@@ -124,7 +124,6 @@ import UserNameDialog from "../components/UserNameDialog.vue";
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { retrieveAccountMetadata } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { parseJsonField } from "../db/databaseUtil";
interface QRScanResult {
rawValue?: string;
@@ -475,9 +474,7 @@ export default class ContactQRScan extends Vue {
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(
parseJsonField(contact.contactMethods, []),
);
contact.contactMethods = JSON.stringify(contact.contactMethods);
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>,
"contacts",

View File

@@ -171,7 +171,6 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import {
generateEndorserJwtUrlForAccount,
@@ -779,9 +778,7 @@ export default class ContactQRScanShow extends Vue {
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(
parseJsonField(contact.contactMethods, []),
);
contact.contactMethods = JSON.stringify(contact.contactMethods);
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>,
"contacts",

View File

@@ -78,7 +78,7 @@
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
/>
<button
class="px-4 rounded-r bg-green-200 border border-green-400"
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
@click="onClickNewContact()"
>
<font-awesome icon="plus" class="fa-fw" />
@@ -86,8 +86,8 @@
</div>
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="">
<div v-if="!showGiveNumbers" class="flex items-center">
<div class="w-full text-left">
<div v-if="!showGiveNumbers">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
@@ -101,33 +101,52 @@
/>
<button
v-if="!showGiveNumbers"
:class="
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style="
contactsSelected.length > 0
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
>
Copy
Copy Selections
</button>
<button @click="showCopySelectionsInfo()">
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
</button>
<font-awesome
icon="circle-info"
class="text-2xl text-blue-500 ml-2"
@click="showCopySelectionsInfo()"
/>
</div>
</div>
<div class="flex items-center gap-2">
<div class="w-full text-right">
<button
v-if="showGiveNumbers"
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{
showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc"
}}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
<div class="w-full text-right">
In the following, only the most recent hours are included. To see more,
click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
>
<font-awesome icon="file-lines" class="fa-fw" />
</span>
<br />
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
@@ -140,25 +159,6 @@
}}
<font-awesome icon="left-right" class="fa-fw" />
</button>
<button
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="toggleShowContactAmounts()"
>
{{ showGiveNumbers ? "Hide Actions" : "See Actions" }}
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="my-3">
<div class="w-full text-center text-sm italic text-slate-600">
Only the most recent hours are included. <br />To see more, click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-0.5 rounded"
>
<font-awesome icon="file-lines" class="text-xs fa-fw" />
</span>
<br />
</div>
</div>
@@ -166,7 +166,7 @@
<ul
v-if="contacts.length > 0"
id="listContacts"
class="border-t border-slate-300 my-2"
class="border-t border-slate-300 mt-1"
>
<li
v-for="contact in filteredContacts()"
@@ -174,125 +174,125 @@
class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem"
>
<div class="flex items-center justify-between gap-3">
<div class="flex overflow-hidden min-w-0 items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<div class="grow overflow-hidden">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<input
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
contactsSelected.indexOf(contact.did),
1,
)
: contactsSelected.push(contact.did)
"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
<div
class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
>
<EntityIcon
:contact="contact"
:icon-size="48"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
</div>
<div class="overflow-hidden">
<h2 class="text-base font-semibold truncate">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }}
</h2>
<div class="flex gap-1.5 items-center overflow-hidden">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-base text-blue-500"
/>
</router-link>
<span>
<div class="flex gap-2 items-center">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-xl text-blue-500"
/>
</router-link>
<span class="text-xs truncate">{{ contact.did }}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</div>
</div>
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-1.5 items-end"
>
<div class="text-center">
<div class="text-xs leading-none mb-1">From/To</div>
<div class="flex items-center">
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
</div>
<span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did)
}}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</span>
</div>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-2 items-center"
>
Offer
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
To:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
}}
</button>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
Offer
</button>
<router-link
:to="{
name: 'contact-amounts',
query: { contactDid: contact.did },
}"
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
</div>
</div>
</div>
</li>
@@ -314,18 +314,16 @@
/>
<button
v-if="!showGiveNumbers"
:class="
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
:style="
contactsSelected.length > 0
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
@click="copySelectedContacts()"
>
Copy
Copy Selections
</button>
</div>
@@ -544,7 +542,7 @@ export default class ContactsView extends Vue {
if (response.status != 201) {
throw { error: { response: response } };
}
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
isRegistered: true,
});
if (USE_DEXIE_DB) {
@@ -1000,6 +998,8 @@ export default class ContactsView extends Vue {
newContact as unknown as Record<string, unknown>,
"contacts",
);
logger.error("sql", sql);
logger.error("params", params);
let contactPromise = platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
// @ts-expect-error since the result of this promise won't be used, and this will go away soon

View File

@@ -788,7 +788,7 @@ export default class DiscoverView extends Vue {
const route = {
path: this.isProjectsActive
? "/project/" + encodeURIComponent(id)
: "/user-profile/" + encodeURIComponent(id),
: "/userProfile/" + encodeURIComponent(id),
};
this.$router.push(route);
}

View File

@@ -826,7 +826,7 @@ export default class GiftedDetails extends Vue {
}
if (!result.success) {
const errorMessage = result.error;
const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result);
this.$notify(
{
@@ -899,6 +899,19 @@ export default class GiftedDetails extends Vue {
// Helper functions for readability
/**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getGiveCreationErrorMessage(result: any) {
return (
result.error?.userMessage ||
result.error?.error ||
result.response?.data?.error?.message
);
}
explainData() {
this.$notify(
{

View File

@@ -24,11 +24,11 @@
<!-- eslint-disable prettier/prettier max-len -->
<div>
<p>
This app focuses on raw gratitude, using it to build cool things together with your network.
This app focuses on gifts & gratitude, using them to build cool things together with your network.
</p>
<p class="ml-4">
If you'd like to see the page-by-page help again,
If you'd like to see the page-by-page help,
<span
class="text-blue-500 cursor-pointer"
@click="unsetFinishedOnboarding()"
@@ -555,6 +555,9 @@
initiative.
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold">
I have other questions or feedback, like getting a new profile or removing my data or requesting an improvement.
</h2>
@@ -564,28 +567,6 @@
>info@TimeSafari.app</a
>
</p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>{{ package.version }} ({{ commitHash }})</p>
<div v-if="Capacitor.isNativePlatform()">
<h2 class="text-xl font-semibold">
Do I have the latest version?
</h2>
<p v-if="Capacitor.getPlatform() === 'ios'">
<a href="https://apps.apple.com/us/app/time-safari/id6742664907" target="_blank" class="text-blue-500">
Check the App Store.
</a>
</p>
<p v-else-if="Capacitor.getPlatform() === 'android'">
<a href="https://timesafari.app/app.apk" target="_blank" class="text-blue-500">
Download the latest APK to see.
</a>
</p>
<p v-else>
Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized.
</p>
</div>
</div>
<!-- eslint enable -->
</section>
@@ -622,7 +603,6 @@ export default class HelpView extends Vue {
showVerifiable = false;
APP_SERVER = APP_SERVER;
Capacitor = Capacitor;
// Ideally, we put no functionality in here, especially in the setup,
// because we never want this page to have a chance of throwing an error.
@@ -642,7 +622,7 @@ export default class HelpView extends Vue {
}
if (settings.activeDid) {
await databaseUtil.updateDidSpecificSettings(settings.activeDid, {
await databaseUtil.updateAccountSettings(settings.activeDid, {
finishedOnboarding: false,
});
if (USE_DEXIE_DB) {

View File

@@ -630,7 +630,7 @@ export default class HomeView extends Vue {
this.activeDid,
);
if (resp.status === 200) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
isRegistered: true,
...(await databaseUtil.retrieveSettingsForActiveAccount()),
});
@@ -785,7 +785,7 @@ export default class HomeView extends Vue {
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
apiServer: this.apiServer,
isRegistered: true,
...settings,
@@ -1843,7 +1843,7 @@ export default class HomeView extends Vue {
this.axios,
);
if (result.success) {
if (result.type === "success") {
this.$notify(
{
group: "alert",

View File

@@ -79,8 +79,7 @@ import {
newIdentifier,
nextDerivationPath,
} from "../libs/crypto";
import * as databaseUtil from "../db/databaseUtil";
import { db } from "../db/index";
import { accountsDBPromise, db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import {
retrieveAllAccountsMetadata,
@@ -165,15 +164,23 @@ export default class ImportAccountView extends Vue {
try {
await saveNewIdentity(newId, mne, newDerivPath);
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: newDerivPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
});
}
// record that as the active DID
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("UPDATE settings SET activeDid = ?", [
newId.did,
]);
await databaseUtil.updateDidSpecificSettings(newId.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,

View File

@@ -257,7 +257,7 @@ export default class NewActivityView extends Vue {
async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
if (USE_DEXIE_DB) {
@@ -285,7 +285,7 @@ export default class NewActivityView extends Vue {
);
if (index !== -1 && index < this.newOffersToUser.length - 1) {
// Set to the next offer's jwtId
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
});
if (USE_DEXIE_DB) {
@@ -295,7 +295,7 @@ export default class NewActivityView extends Vue {
}
} else {
// it's the last entry (or not found), so just keep it the same
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
});
if (USE_DEXIE_DB) {
@@ -319,7 +319,7 @@ export default class NewActivityView extends Vue {
this.showOffersToUserProjectsDetails =
!this.showOffersToUserProjectsDetails;
if (this.showOffersToUserProjectsDetails) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
@@ -349,7 +349,7 @@ export default class NewActivityView extends Vue {
);
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
// Set to the next offer's jwtId
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[index + 1].jwtId,
});
@@ -361,7 +361,7 @@ export default class NewActivityView extends Vue {
}
} else {
// it's the last entry (or not found), so just keep it the same
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId,
});

View File

@@ -52,24 +52,16 @@
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
{{ issuerInfoObject?.displayName }}
</span>
<span class="inline-flex items-center">
<router-link
:to="{
path: '/did/' + encodeURIComponent(issuer),
}"
class="text-blue-500 ml-1"
title="See more about this person"
>
{{ issuerInfoObject?.displayName }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
<a :href="`/did/${issuer}`" class="text-blue-500">
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</a>
</span>
<span v-if="serverUtil.isHiddenDid(issuer)" class="ml-1">
<span v-else-if="serverUtil.isHiddenDid(issuer)">
<font-awesome
icon="info-circle"
class="fa-fw text-blue-500 cursor-pointer"
@@ -1433,7 +1425,7 @@ export default class ProjectViewView extends Vue {
this.apiServer,
this.axios,
);
if (result.success) {
if (result.type === "success") {
this.$notify(
{
group: "alert",

View File

@@ -155,7 +155,7 @@ import { Contact } from "../db/tables/contacts";
import {
GenericCredWrapper,
GenericVerifiableCredential,
CreateAndSubmitClaimResult,
ErrorResult,
} from "../interfaces";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
@@ -298,13 +298,13 @@ export default class QuickActionBvcBeginView extends Vue {
}
// in parallel, make a confirmation for each selected claim and send them all to the server
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] = await Promise.allSettled(
const confirmResults = await Promise.allSettled(
this.claimsToConfirmSelected.map(async (jwtId) => {
const record = this.claimsToConfirm.find(
(claim) => claim.id === jwtId,
);
if (!record) {
return { success: false, error: "Record not found." };
return { type: "error", error: "Record not found." };
}
return createAndSubmitConfirmation(
this.activeDid,
@@ -318,8 +318,8 @@ export default class QuickActionBvcBeginView extends Vue {
);
// check for any rejected confirmations
const confirmsSucceeded = confirmResults.filter(
// 'fulfilled' is the status in a successful PromiseFulfilledResult
(result) => result.status === "fulfilled" && result.value.success,
(result) =>
result.status === "fulfilled" && result.value.type === "success",
);
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
logger.error("Error sending confirmations:", confirmResults);
@@ -353,7 +353,7 @@ export default class QuickActionBvcBeginView extends Vue {
undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
giveSucceeded = giveResult.success;
giveSucceeded = giveResult.type === "success";
if (!giveSucceeded) {
logger.error("Error sending give:", giveResult);
this.$notify(
@@ -362,7 +362,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger",
title: "Error",
text:
(giveResult as CreateAndSubmitClaimResult)?.error ||
(giveResult as ErrorResult)?.error?.userMessage ||
"There was an error sending that give.",
},
5000,

View File

@@ -215,7 +215,7 @@ export default class SearchAreaView extends Vue {
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: searchBoxes as unknown, // Type assertion for Dexie compatibility
searchBoxes: [newSearchBox],
});
}
this.searchBox = newSearchBox;
@@ -269,7 +269,7 @@ export default class SearchAreaView extends Vue {
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
searchBoxes: "[]" as unknown as string, // Type assertion for Dexie compatibility
searchBoxes: [],
filterFeedByNearby: false,
});
}