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 icons
android/app/src/main/res/

View File

@@ -321,11 +321,11 @@ Prerequisites: macOS with Xcode installed
#### Each Release #### Each Release
0. First time (or if dependencies change): 0. First time (or if XCode dependencies change):
- `pkgx +rubygems.org sh` - `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 ```bash
gem_path=$(which gem) gem_path=$(which gem)
@@ -334,9 +334,12 @@ Prerequisites: macOS with Xcode installed
export GEM_PATH=$shortened_path 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 ```bash
rm -rf dist rm -rf dist
@@ -344,7 +347,8 @@ Prerequisites: macOS with Xcode installed
npm run build:capacitor npm run build:capacitor
``` ```
3. Update iOS project with latest build:
2. Update iOS project with latest build:
```bash ```bash
npx cap sync ios 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. - If that fails with "Could not find..." then look at the "gem_path" instructions above.
4. Copy the assets: 3. Copy the assets:
```bash ```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents. # 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 npx capacitor-assets generate --ios
``` ```
4. Bump the version to match Android & package.json: 4. Bump the version to match Android:
``` ```
cd ios/App cd ios/App
xcrun agvtool new-version 30 xcrun agvtool new-version 25
# 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.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 - cd -
``` ```
@@ -398,8 +403,6 @@ Prerequisites: macOS with Xcode installed
* 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".
8. Revert the iOS flag isIOS in CapacitorPlatformService.
### Android Build ### Android Build
Prerequisites: Android Studio with Java SDK installed Prerequisites: Android Studio with Java SDK installed
@@ -424,7 +427,7 @@ Prerequisites: Android Studio with Java SDK installed
npx capacitor-assets generate --android 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: 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. - 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: You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
@@ -487,5 +490,3 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
<data android:scheme="timesafari" /> <data android:scheme="timesafari" />
</intent-filter> </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" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 30 versionCode 25
versionName "0.5.4" versionName "0.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -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 = 30; CURRENT_PROJECT_VERSION = 25;
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.4; MARKETING_VERSION = 0.5.0;
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 = 30; CURRENT_PROJECT_VERSION = 25;
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.4; MARKETING_VERSION = 0.5.0;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

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

1215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -321,7 +321,7 @@ export default class GiftedDialog extends Vue {
); );
if (!result.success) { if (!result.success) {
const errorMessage = result.error; const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result); logger.error("Error with give creation result:", result);
this.$notify( this.$notify(
{ {
@@ -367,6 +367,19 @@ export default class GiftedDialog extends Vue {
// Helper functions for readability // 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() { explainData() {
this.$notify( this.$notify(
{ {

View File

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

View File

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

View File

@@ -250,7 +250,7 @@ export default class OfferDialog extends Vue {
); );
if (!result.success) { if (!result.success) {
const errorMessage = result.error; const errorMessage = this.getOfferCreationErrorMessage(result);
logger.error("Error with offer creation result:", result); logger.error("Error with offer creation result:", result);
this.$notify( 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> </script>

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
import { AxiosResponse } from "axios";
import { GiverReceiverInputInfo } from "../libs/util"; import { GiverReceiverInputInfo } from "../libs/util";
import { ErrorResult, ResultWithType } from "./common";
export interface GiverOutputInfo { export interface GiverOutputInfo {
action: string; action: string;
@@ -45,3 +47,12 @@ export interface ProviderInfo {
*/ */
linkConfirmed: boolean; 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>; publicUrls?: Record<string, string>;
} }
export interface ResultWithType {
type: string;
}
export interface ErrorResponse { export interface ErrorResponse {
error?: { error?: {
message?: string; message?: string;
@@ -26,6 +30,11 @@ export interface InternalError {
userMessage?: string; userMessage?: string;
} }
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export interface KeyMeta { export interface KeyMeta {
did: string; did: string;
publicKeyHex: string; publicKeyHex: string;

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@
* *
* Supported Routes: * Supported Routes:
* - user-profile: View user profile * - user-profile: View user profile
* - project: View project details * - project-details: View project details
* - onboard-meeting-setup: Setup onboarding meeting * - onboard-meeting-setup: Setup onboarding meeting
* - invite-one-accept: Accept invitation * - invite-one-accept: Accept invitation
* - contact-import: Import contacts * - contact-import: Import contacts
@@ -81,16 +81,18 @@ export class DeepLinkHandler {
string, string,
{ name: string; paramKey?: 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" }, "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, CameraSource,
CameraDirection, CameraDirection,
} from "@capacitor/camera"; } from "@capacitor/camera";
import { Capacitor } from "@capacitor/core";
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
import { import {
SQLiteConnection, SQLiteConnection,
@@ -248,7 +247,7 @@ export class CapacitorPlatformService implements PlatformService {
hasFileSystem: true, hasFileSystem: true,
hasCamera: true, hasCamera: true,
isMobile: true, isMobile: true,
isIOS: Capacitor.getPlatform() === "ios", isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: false, hasFileDownload: false,
needsFileHandlingInstructions: true, needsFileHandlingInstructions: true,
isNativeApp: true, isNativeApp: true,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -213,7 +213,6 @@ import {
} from "../db/index"; } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts"; import { Contact, ContactMethod } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil"; import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { import {
capitalizeAndInsertSpacesBeforeCaps, capitalizeAndInsertSpacesBeforeCaps,
@@ -290,7 +289,7 @@ function dbRecordToContact(record: ContactDbRecord): Contact {
profileImageUrl: safeString(record.profileImageUrl), profileImageUrl: safeString(record.profileImageUrl),
publicKeyBase64: safeString(record.publicKeyBase64), publicKeyBase64: safeString(record.publicKeyBase64),
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64), 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 { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { retrieveAccountMetadata } from "../libs/util"; import { retrieveAccountMetadata } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { parseJsonField } from "../db/databaseUtil";
interface QRScanResult { interface QRScanResult {
rawValue?: string; rawValue?: string;
@@ -475,9 +474,7 @@ export default class ContactQRScan extends Vue {
// Add new contact // Add new contact
// @ts-expect-error because we're just using the value to store to the DB // @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify( contact.contactMethods = JSON.stringify(contact.contactMethods);
parseJsonField(contact.contactMethods, []),
);
const { sql, params } = databaseUtil.generateInsertStatement( const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>, contact as unknown as Record<string, unknown>,
"contacts", "contacts",

View File

@@ -171,7 +171,6 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil"; import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { import {
generateEndorserJwtUrlForAccount, generateEndorserJwtUrlForAccount,
@@ -779,9 +778,7 @@ export default class ContactQRScanShow extends Vue {
// Add new contact // Add new contact
// @ts-expect-error because we're just using the value to store to the DB // @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify( contact.contactMethods = JSON.stringify(contact.contactMethods);
parseJsonField(contact.contactMethods, []),
);
const { sql, params } = databaseUtil.generateInsertStatement( const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>, contact as unknown as Record<string, unknown>,
"contacts", "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" class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
/> />
<button <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()" @click="onClickNewContact()"
> >
<font-awesome icon="plus" class="fa-fw" /> <font-awesome icon="plus" class="fa-fw" />
@@ -86,8 +86,8 @@
</div> </div>
<div v-if="contacts.length > 0" class="flex justify-between"> <div v-if="contacts.length > 0" class="flex justify-between">
<div class=""> <div class="w-full text-left">
<div v-if="!showGiveNumbers" class="flex items-center"> <div v-if="!showGiveNumbers">
<input <input
type="checkbox" type="checkbox"
:checked="contactsSelected.length === contacts.length" :checked="contactsSelected.length === contacts.length"
@@ -101,33 +101,52 @@
/> />
<button <button
v-if="!showGiveNumbers" 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 contactsSelected.length > 0
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' + ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' + : 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
'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'
" "
data-testId="copySelectedContactsButtonTop" data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()" @click="copySelectedContacts()"
> >
Copy Copy Selections
</button> </button>
<button @click="showCopySelectionsInfo()">
<font-awesome <font-awesome
icon="circle-info" icon="circle-info"
class="text-2xl text-blue-500 ml-2" class="text-xl text-blue-500 ml-4"
@click="showCopySelectionsInfo()"
/> />
</button>
</div> </div>
</div> </div>
<div class="flex items-center gap-2"> <div class="w-full text-right">
<button <button
v-if="showGiveNumbers"
href="" 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()" :class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()" @click="toggleShowGiveTotals()"
> >
@@ -140,25 +159,6 @@
}} }}
<font-awesome icon="left-right" class="fa-fw" /> <font-awesome icon="left-right" class="fa-fw" />
</button> </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>
</div> </div>
@@ -166,7 +166,7 @@
<ul <ul
v-if="contacts.length > 0" v-if="contacts.length > 0"
id="listContacts" id="listContacts"
class="border-t border-slate-300 my-2" class="border-t border-slate-300 mt-1"
> >
<li <li
v-for="contact in filteredContacts()" v-for="contact in filteredContacts()"
@@ -174,8 +174,9 @@
class="border-b border-slate-300 pt-1 pb-1" class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem" data-testId="contactListItem"
> >
<div class="grow overflow-hidden">
<div class="flex items-center justify-between gap-3"> <div class="flex items-center justify-between gap-3">
<div class="flex overflow-hidden min-w-0 items-center gap-3"> <div class="flex items-center gap-3">
<input <input
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
type="checkbox" type="checkbox"
@@ -192,26 +193,23 @@
" "
/> />
<div
class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
>
<EntityIcon <EntityIcon
:contact="contact" :contact="contact"
:icon-size="48" :icon-size="48"
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden" class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact" @click="showLargeIdenticon = contact"
/> />
</div>
<div class="overflow-hidden"> <h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
<h2 class="text-base font-semibold truncate">
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }} {{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
</h2> </h2>
<div class="flex gap-1.5 items-center overflow-hidden"> <span>
<div class="flex gap-2 items-center">
<router-link <router-link
:to="{ :to="{
path: '/did/' + encodeURIComponent(contact.did), path: '/did/' + encodeURIComponent(contact.did),
@@ -220,30 +218,31 @@
> >
<font-awesome <font-awesome
icon="circle-info" icon="circle-info"
class="text-base text-blue-500" class="text-xl text-blue-500"
/> />
</router-link> </router-link>
<span class="text-xs truncate">{{ contact.did }}</span> <span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did)
}}</span>
</div> </div>
<div class="text-sm"> <div class="text-sm">
{{ contact.notes }} {{ contact.notes }}
</div> </div>
</div> </span>
</div> </div>
<div <div
v-if="showGiveNumbers && contact.did != activeDid" v-if="showGiveNumbers && contact.did != activeDid"
class="flex gap-1.5 items-end" class="flex gap-2 items-center"
> >
<div class="text-center">
<div class="text-xs leading-none mb-1">From/To</div>
<div class="flex items-center">
<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-l-md" 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] || ''" :title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)" @click="confirmShowGiftedDialog(contact.did, activeDid)"
> >
From:
<br />
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
showGiveTotals showGiveTotals
@@ -257,10 +256,12 @@
</button> </button>
<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" 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] || ''" :title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)" @click="confirmShowGiftedDialog(activeDid, contact.did)"
> >
To:
<br />
{{ {{
/* eslint-disable prettier/prettier */ /* eslint-disable prettier/prettier */
showGiveTotals showGiveTotals
@@ -272,11 +273,9 @@
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
}} }}
</button> </button>
</div>
</div>
<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" 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" data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)" @click="openOfferDialog(contact.did, contact.name)"
> >
@@ -288,13 +287,14 @@
name: 'contact-amounts', name: 'contact-amounts',
query: { contactDid: contact.did }, 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" 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" title="See more given activity"
> >
<font-awesome icon="file-lines" class="fa-fw" /> <font-awesome icon="file-lines" class="fa-fw" />
</router-link> </router-link>
</div> </div>
</div> </div>
</div>
</li> </li>
</ul> </ul>
<p v-else>There are no contacts.</p> <p v-else>There are no contacts.</p>
@@ -314,18 +314,16 @@
/> />
<button <button
v-if="!showGiveNumbers" 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 contactsSelected.length > 0
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' + ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' + : 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
'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'
" "
@click="copySelectedContacts()" @click="copySelectedContacts()"
> >
Copy Copy Selections
</button> </button>
</div> </div>
@@ -544,7 +542,7 @@ export default class ContactsView extends Vue {
if (response.status != 201) { if (response.status != 201) {
throw { error: { response: response } }; throw { error: { response: response } };
} }
await databaseUtil.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
isRegistered: true, isRegistered: true,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
@@ -1000,6 +998,8 @@ export default class ContactsView extends Vue {
newContact as unknown as Record<string, unknown>, newContact as unknown as Record<string, unknown>,
"contacts", "contacts",
); );
logger.error("sql", sql);
logger.error("params", params);
let contactPromise = platformService.dbExec(sql, params); let contactPromise = platformService.dbExec(sql, params);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
// @ts-expect-error since the result of this promise won't be used, and this will go away soon // @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 = { const route = {
path: this.isProjectsActive path: this.isProjectsActive
? "/project/" + encodeURIComponent(id) ? "/project/" + encodeURIComponent(id)
: "/user-profile/" + encodeURIComponent(id), : "/userProfile/" + encodeURIComponent(id),
}; };
this.$router.push(route); this.$router.push(route);
} }

View File

@@ -826,7 +826,7 @@ export default class GiftedDetails extends Vue {
} }
if (!result.success) { if (!result.success) {
const errorMessage = result.error; const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result); logger.error("Error with give creation result:", result);
this.$notify( this.$notify(
{ {
@@ -899,6 +899,19 @@ export default class GiftedDetails extends Vue {
// Helper functions for readability // 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() { explainData() {
this.$notify( this.$notify(
{ {

View File

@@ -24,11 +24,11 @@
<!-- eslint-disable prettier/prettier max-len --> <!-- eslint-disable prettier/prettier max-len -->
<div> <div>
<p> <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>
<p class="ml-4"> <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 <span
class="text-blue-500 cursor-pointer" class="text-blue-500 cursor-pointer"
@click="unsetFinishedOnboarding()" @click="unsetFinishedOnboarding()"
@@ -555,6 +555,9 @@
initiative. initiative.
</p> </p>
<h2 class="text-xl font-semibold">What app version is this?</h2>
<p>{{ package.version }} ({{ commitHash }})</p>
<h2 class="text-xl font-semibold"> <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. I have other questions or feedback, like getting a new profile or removing my data or requesting an improvement.
</h2> </h2>
@@ -564,28 +567,6 @@
>info@TimeSafari.app</a >info@TimeSafari.app</a
> >
</p> </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> </div>
<!-- eslint enable --> <!-- eslint enable -->
</section> </section>
@@ -622,7 +603,6 @@ export default class HelpView extends Vue {
showVerifiable = false; showVerifiable = false;
APP_SERVER = APP_SERVER; APP_SERVER = APP_SERVER;
Capacitor = Capacitor;
// Ideally, we put no functionality in here, especially in the setup, // 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. // 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) { if (settings.activeDid) {
await databaseUtil.updateDidSpecificSettings(settings.activeDid, { await databaseUtil.updateAccountSettings(settings.activeDid, {
finishedOnboarding: false, finishedOnboarding: false,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {

View File

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

View File

@@ -79,8 +79,7 @@ import {
newIdentifier, newIdentifier,
nextDerivationPath, nextDerivationPath,
} from "../libs/crypto"; } from "../libs/crypto";
import * as databaseUtil from "../db/databaseUtil"; import { accountsDBPromise, db } from "../db/index";
import { db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { import {
retrieveAllAccountsMetadata, retrieveAllAccountsMetadata,
@@ -165,15 +164,23 @@ export default class ImportAccountView extends Vue {
try { try {
await saveNewIdentity(newId, mne, newDerivPath); 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 // record that as the active DID
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("UPDATE settings SET activeDid = ?", [ await platformService.dbExec("UPDATE settings SET activeDid = ?", [
newId.did, newId.did,
]); ]);
await databaseUtil.updateDidSpecificSettings(newId.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did, activeDid: newId.did,

View File

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

View File

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

View File

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

View File

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