Compare commits

...

15 Commits

Author SHA1 Message Date
11f122552d chore: bump to version 1.1.3 number 48 2025-11-19 19:58:48 -07:00
c84a3b6705 add instructions to connect to any user profile (#224)
See https://app.clickup.com/t/86b76734v

Reviewed-on: #224
Co-authored-by: Trent Larson <trent@trentlarson.com>
Co-committed-by: Trent Larson <trent@trentlarson.com>
2025-11-19 18:58:49 +00:00
e64902321f Merge pull request 'fix(GiftedDialog): preserve recipient when changing giver project' (#225) from gifted-dialog-recipient-fix into master
Reviewed-on: #225
2025-11-19 09:56:55 +00:00
7abce8f95c fix: don't count any changed projects on the front page that had blank differences 2025-11-18 19:53:52 -07:00
88dce4d100 fix: show a "project changed" entry if the server reports something 2025-11-18 19:49:40 -07:00
Jose Olarte III
c4eb6f2d1d fix(GiftedDialog): preserve recipient when changing giver project
Modified selectProject() to only set receiver to "You" if no receiver
has been selected yet, preventing recipient from being reset when
changing giver project in Project-to-Person context.
2025-11-18 15:50:11 +08:00
06fdaff879 Merge pull request 'entitygrid-infinite-scroll-improvements' (#223) from entitygrid-infinite-scroll-improvements into master
Reviewed-on: #223
2025-11-18 06:56:55 +00:00
8024a3d02a Merge pull request 'meeting-project-dialog' (#222) from meeting-project-dialog into master
Reviewed-on: #222
2025-11-18 06:56:23 +00:00
83b470e28a fix: link from DID page to Help 2025-11-16 15:35:19 -07:00
1739567b18 Merge pull request 'feat: replace authorized representative input with contact selection dialog' (#219) from project-representative-dialog into master
Reviewed-on: #219
2025-11-12 01:42:48 -05:00
5050156beb fix: a type, plus add the type-check to the mobile build scripts 2025-11-08 08:31:42 -07:00
d265a9f78c chore: bump version and add "-beta" 2025-11-06 08:56:33 -07:00
f848de15f1 chore: bump version to 1.1.2 build 47 (for fix to seed backup) 2025-11-06 08:54:11 -07:00
ebaf2dedf0 Merge pull request 'fix: database connection error causing navigation redirect on iOS/Android' (#220) from fix-sqlite-connection-error-mobile into master
Reviewed-on: #220
2025-11-06 09:52:31 -05:00
Jose Olarte III
749204f96b fix: database connection error causing navigation redirect on iOS/Android
Handle "Connection already exists" error when initializing SQLite database
on Capacitor platforms. The native connection can persist across app
restarts while the JavaScript connection Map is empty, causing a mismatch.

When createConnection fails with "already exists":
- Check if connection exists in JavaScript Map and retrieve it if present
- If not in Map, close the native connection and recreate to sync both sides
- Handle "already open" errors gracefully when opening existing connections

This fixes the issue where clicking "Backup Identifier Seed" would redirect
to StartView instead of SeedBackupView due to database initialization
failures in the router navigation guard.

Fixes navigation issue on both iOS and Android platforms.
2025-11-06 21:38:51 +08:00
19 changed files with 572 additions and 103 deletions

View File

@@ -1151,28 +1151,28 @@ If you need to build manually or want to understand the individual steps:
- ... 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)
shortened_path="${gem_path:h:h}" shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path export GEM_PATH=$shortened_path
``` ```
##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version; ##### 1. Bump the version in package.json for `MARKETING_VERSION`, then `grep CURRENT_PROJECT_VERSION ios/App/App.xcodeproj/project.pbxproj` and add 1 for the numbered version;
```bash ```bash
cd ios/App && xcrun agvtool new-version 46 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.1;/g" App.xcodeproj/project.pbxproj && cd - cd ios/App && xcrun agvtool new-version 48 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.3;/g" App.xcodeproj/project.pbxproj && cd -
# 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
``` ```
##### 2. Build ##### 2. Build
Here's prod. Also available: test, dev Here's prod. Also available: test, dev
```bash ```bash
npm run build:ios:prod npm run build:ios:prod
``` ```
3.1. Use Xcode to build and run on simulator or device. 3.1. Use Xcode to build and run on simulator or device.
@@ -1197,7 +1197,8 @@ If you need to build manually or want to understand the individual steps:
- It can take 15 minutes for the build to show up in the list of builds. - It can take 15 minutes for the build to show up in the list of builds.
- You'll probably have to "Manage" something about encryption, disallowed in France. - You'll probably have to "Manage" something about encryption, disallowed in France.
- Then "Save" and "Add to Review" and "Resubmit to App Review". - Then "Save" and "Add to Review" and "Resubmit to App Review".
- Eventually it'll be "Ready for Distribution" which means - Eventually it'll be "Ready for Distribution" which means it's live
- When finished, bump package.json version
### Android Build ### Android Build
@@ -1303,8 +1304,8 @@ The recommended way to build for Android is using the automated build script:
# Standard build and open Android Studio # Standard build and open Android Studio
./scripts/build-android.sh ./scripts/build-android.sh
# Build with specific version numbers # Build with specific version numbers -- doesn't change source files
./scripts/build-android.sh --version 1.0.3 --build-number 35 #./scripts/build-android.sh --version 1.1.3 --build-number 48
# Build without opening Android Studio (for CI/CD) # Build without opening Android Studio (for CI/CD)
./scripts/build-android.sh --no-studio ./scripts/build-android.sh --no-studio
@@ -1315,26 +1316,26 @@ The recommended way to build for Android is using the automated build script:
#### Android Manual Build Process #### Android Manual Build Process
##### 1. Bump the version in package.json, then here: android/app/build.gradle ##### 1. Bump the version in package.json, then update these versions & run:
```bash ```bash
perl -p -i -e 's/versionCode .*/versionCode 46/g' android/app/build.gradle perl -p -i -e 's/versionCode .*/versionCode 48/g' android/app/build.gradle
perl -p -i -e 's/versionName .*/versionName "1.1.1"/g' android/app/build.gradle perl -p -i -e 's/versionName .*/versionName "1.1.3"/g' android/app/build.gradle
``` ```
##### 2. Build ##### 2. Build
Here's prod. Also available: test, dev Here's prod. Also available: test, dev
```bash ```bash
npm run build:android:prod npm run build:android:prod
``` ```
##### 3. Open the project in Android Studio ##### 3. Open the project in Android Studio
```bash ```bash
npx cap open android npx cap open android
``` ```
##### 4. Use Android Studio to build and run on emulator or device ##### 4. Use Android Studio to build and run on emulator or device
@@ -1379,6 +1380,8 @@ At play.google.com/console:
- Note that if you add testers, you have to go to "Publishing Overview" and send - 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. those changes or your (closed) testers won't see it.
- When finished, bump package.json version
### Capacitor Operations ### Capacitor Operations
```bash ```bash

View File

@@ -6,6 +6,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.1.3] - 2025.11.19
### Changed
- Project selection in dialogs now reaches out to server when filtering
- Project selection during onboarding meeting is a search (not an input box)
- Improve the switching of agent when agent edits a project
### Fixed
- Reassignment of "you" as recipient when changing giver project
- Bad counts for project-change notification on front page
## [1.1.2] - 2025.11.06
### Fixed
- Bad page when user follows prompt to backup seed
## [1.1.1] - 2025.11.03 ## [1.1.1] - 2025.11.03
### Added ### Added

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 46 versionCode 48
versionName "1.1.1" versionName "1.1.3"
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 = 46; CURRENT_PROJECT_VERSION = 48;
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 = 1.1.1; MARKETING_VERSION = 1.1.3;
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 = 46; CURRENT_PROJECT_VERSION = 48;
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 = 1.1.1; MARKETING_VERSION = 1.1.3;
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 = "";

24
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.2-beta", "version": "1.1.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "1.1.2-beta", "version": "1.1.3",
"dependencies": { "dependencies": {
"@capacitor-community/electron": "^5.0.1", "@capacitor-community/electron": "^5.0.1",
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.2",
@@ -27,6 +27,7 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",
@@ -6789,6 +6790,25 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/@fortawesome/free-brands-svg-icons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz",
"integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==",
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.1.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-brands-svg-icons/node_modules/@fortawesome/fontawesome-common-types": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
"integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
"engines": {
"node": ">=6"
}
},
"node_modules/@fortawesome/free-regular-svg-icons": { "node_modules/@fortawesome/free-regular-svg-icons": {
"version": "6.7.2", "version": "6.7.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "1.1.2-beta", "version": "1.1.3",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@@ -156,6 +156,7 @@
"@ethersproject/hdnode": "^5.7.0", "@ethersproject/hdnode": "^5.7.0",
"@ethersproject/wallet": "^5.8.0", "@ethersproject/wallet": "^5.8.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/vue-fontawesome": "^3.0.6", "@fortawesome/vue-fontawesome": "^3.0.6",

View File

@@ -436,7 +436,21 @@ fi
log_info "Cleaning dist directory..." log_info "Cleaning dist directory..."
clean_build_artifacts "dist" clean_build_artifacts "dist"
# Step 4: Build Capacitor version with mode # Step 4: Run TypeScript type checking for test and production builds
if [ "$BUILD_MODE" = "production" ] || [ "$BUILD_MODE" = "test" ]; then
log_info "Running TypeScript type checking for $BUILD_MODE mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $BUILD_MODE mode!"
exit 2
fi
log_success "TypeScript type checking completed for $BUILD_MODE mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
# Step 5: Build Capacitor version with mode
if [ "$BUILD_MODE" = "development" ]; then if [ "$BUILD_MODE" = "development" ]; then
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3 safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
elif [ "$BUILD_MODE" = "test" ]; then elif [ "$BUILD_MODE" = "test" ]; then
@@ -445,23 +459,23 @@ elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3 safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi fi
# Step 5: Clean Gradle build # Step 6: Clean Gradle build
safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4 safe_execute "Cleaning Gradle build" "cd android && ./gradlew clean && cd .." || exit 4
# Step 6: Build based on type # Step 7: Build based on type
if [ "$BUILD_TYPE" = "debug" ]; then if [ "$BUILD_TYPE" = "debug" ]; then
safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5 safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5
elif [ "$BUILD_TYPE" = "release" ]; then elif [ "$BUILD_TYPE" = "release" ]; then
safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5 safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5
fi fi
# Step 7: Sync with Capacitor # Step 8: Sync with Capacitor
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6 safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
# Step 8: Generate assets # Step 9: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7 safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7
# Step 9: Build APK/AAB if requested # Step 10: Build APK/AAB if requested
if [ "$BUILD_APK" = true ]; then if [ "$BUILD_APK" = true ]; then
if [ "$BUILD_TYPE" = "debug" ]; then if [ "$BUILD_TYPE" = "debug" ]; then
safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5 safe_execute "Building debug APK" "cd android && ./gradlew assembleDebug && cd .." || exit 5
@@ -474,7 +488,7 @@ if [ "$BUILD_AAB" = true ]; then
safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5 safe_execute "Building AAB" "cd android && ./gradlew bundleRelease && cd .." || exit 5
fi fi
# Step 10: Auto-run app if requested # Step 11: Auto-run app if requested
if [ "$AUTO_RUN" = true ]; then if [ "$AUTO_RUN" = true ]; then
log_step "Auto-running Android app..." log_step "Auto-running Android app..."
safe_execute "Launching app" "npx cap run android" || { safe_execute "Launching app" "npx cap run android" || {
@@ -485,7 +499,7 @@ if [ "$AUTO_RUN" = true ]; then
log_success "Android app launched successfully!" log_success "Android app launched successfully!"
fi fi
# Step 11: Open Android Studio if requested # Step 12: Open Android Studio if requested
if [ "$OPEN_STUDIO" = true ]; then if [ "$OPEN_STUDIO" = true ]; then
safe_execute "Opening Android Studio" "npx cap open android" || exit 8 safe_execute "Opening Android Studio" "npx cap open android" || exit 8
fi fi

View File

@@ -381,7 +381,21 @@ safe_execute "Cleaning iOS build" "clean_ios_build" || exit 1
log_info "Cleaning dist directory..." log_info "Cleaning dist directory..."
clean_build_artifacts "dist" clean_build_artifacts "dist"
# Step 4: Build Capacitor version with mode # Step 4: Run TypeScript type checking for test and production builds
if [ "$BUILD_MODE" = "production" ] || [ "$BUILD_MODE" = "test" ]; then
log_info "Running TypeScript type checking for $BUILD_MODE mode..."
if ! measure_time npm run type-check; then
log_error "TypeScript type checking failed for $BUILD_MODE mode!"
exit 2
fi
log_success "TypeScript type checking completed for $BUILD_MODE mode"
else
log_debug "Skipping TypeScript type checking for development mode"
fi
# Step 5: Build Capacitor version with mode
if [ "$BUILD_MODE" = "development" ]; then if [ "$BUILD_MODE" = "development" ]; then
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3 safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
elif [ "$BUILD_MODE" = "test" ]; then elif [ "$BUILD_MODE" = "test" ]; then
@@ -390,16 +404,16 @@ elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3 safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi fi
# Step 5: Sync with Capacitor # Step 6: Sync with Capacitor
safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6 safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
# Step 6: Generate assets # Step 7: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7 safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
# Step 7: Build iOS app # Step 8: Build iOS app
safe_execute "Building iOS app" "build_ios_app" || exit 5 safe_execute "Building iOS app" "build_ios_app" || exit 5
# Step 8: Build IPA/App if requested # Step 9: Build IPA/App if requested
if [ "$BUILD_IPA" = true ]; then if [ "$BUILD_IPA" = true ]; then
log_info "Building IPA package..." log_info "Building IPA package..."
cd ios/App cd ios/App
@@ -426,12 +440,12 @@ if [ "$BUILD_APP" = true ]; then
log_success "App bundle built successfully" log_success "App bundle built successfully"
fi fi
# Step 9: Auto-run app if requested # Step 10: Auto-run app if requested
if [ "$AUTO_RUN" = true ]; then if [ "$AUTO_RUN" = true ]; then
safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9 safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9
fi fi
# Step 10: Open Xcode if requested # Step 11: Open Xcode if requested
if [ "$OPEN_STUDIO" = true ]; then if [ "$OPEN_STUDIO" = true ]; then
safe_execute "Opening Xcode" "npx cap open ios" || exit 8 safe_execute "Opening Xcode" "npx cap open ios" || exit 8
fi fi

View File

@@ -483,10 +483,13 @@ export default class GiftedDialog extends Vue {
image: project.image, image: project.image,
handleId: project.handleId, handleId: project.handleId,
}; };
this.receiver = { // Only set receiver to "You" if no receiver has been selected yet
did: this.activeDid, if (!this.receiver || !this.receiver.did) {
name: "You", this.receiver = {
}; did: this.activeDid,
name: "You",
};
}
this.firstStep = false; this.firstStep = false;
} }

29
src/constants/contacts.ts Normal file
View File

@@ -0,0 +1,29 @@
/**
* Constants for contact-related functionality
* Created: 2025-11-16
*/
/**
* Contact method types with user-friendly labels
* Used in: ContactEditView.vue, DIDView.vue
*/
export const CONTACT_METHOD_TYPES = [
{ value: "CELL", label: "Mobile" },
{ value: "EMAIL", label: "Email" },
{ value: "WHATSAPP", label: "WhatsApp" },
] as const;
/**
* Type for contact method type values
*/
export type ContactMethodType = (typeof CONTACT_METHOD_TYPES)[number]["value"];
/**
* Helper function to get label for a contact method type
* @param type - The contact method type value (e.g., "CELL", "EMAIL")
* @returns The user-friendly label or the original type if not found
*/
export function getContactMethodLabel(type: string): string {
const methodType = CONTACT_METHOD_TYPES.find((m) => m.value === type);
return methodType ? methodType.label : type;
}

View File

@@ -57,7 +57,12 @@ export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
planName: string; planName: string;
} }
// a summary record; the VC is not currently part of this record /**
* A summary record
* The VC is not currently part of this record.
*
* If you change this, you may want to update NewActivityView.vue to handle differences correctly.
*/
export interface PlanSummaryRecord { export interface PlanSummaryRecord {
agentDid?: string; agentDid?: string;
description: string; description: string;
@@ -76,7 +81,9 @@ export interface PlanSummaryRecord {
export interface PlanSummaryAndPreviousClaim { export interface PlanSummaryAndPreviousClaim {
plan: PlanSummaryRecord; plan: PlanSummaryRecord;
wrappedClaimBefore: GenericCredWrapper<PlanActionClaim>; // This can be undefined, eg. if a project is starred after the stored last-seen-change-jwt ID.
// The endorser-ch test code shows some cases.
wrappedClaimBefore?: GenericCredWrapper<PlanActionClaim>;
} }
/** /**

View File

@@ -1686,7 +1686,10 @@ export async function register(
"Registration thrown error:", "Registration thrown error:",
errorMessage || JSON.stringify(err), errorMessage || JSON.stringify(err),
); );
return { error: errorMessage || "Got a server error when registering." }; return {
error:
(errorMessage as string) || "Got a server error when registering.",
};
} }
return { error: "Got a server error when registering." }; return { error: "Got a server error when registering." };
} }

View File

@@ -91,16 +91,92 @@ export class CapacitorPlatformService
} }
try { try {
// Create/Open database // Try to create/Open database connection
this.db = await this.sqlite.createConnection( try {
this.dbName, this.db = await this.sqlite.createConnection(
false, this.dbName,
"no-encryption", false,
1, "no-encryption",
false, 1,
); false,
);
} catch (createError: unknown) {
// If connection already exists, try to retrieve it or handle gracefully
const errorMessage =
createError instanceof Error
? createError.message
: String(createError);
const errorObj =
typeof createError === "object" && createError !== null
? (createError as { errorMessage?: string; message?: string })
: {};
await this.db.open(); const fullErrorMessage =
errorObj.errorMessage || errorObj.message || errorMessage;
if (fullErrorMessage.includes("already exists")) {
logger.debug(
"[CapacitorPlatformService] Connection already exists on native side, attempting to retrieve",
);
// Check if connection exists in JavaScript Map
const isConnResult = await this.sqlite.isConnection(
this.dbName,
false,
);
if (isConnResult.result) {
// Connection exists in Map, retrieve it
this.db = await this.sqlite.retrieveConnection(this.dbName, false);
logger.debug(
"[CapacitorPlatformService] Successfully retrieved existing connection from Map",
);
} else {
// Connection exists on native side but not in JavaScript Map
// This can happen when the app is restarted but native connections persist
// Try to close the native connection first, then create a new one
logger.debug(
"[CapacitorPlatformService] Connection exists natively but not in Map, closing and recreating",
);
try {
await this.sqlite.closeConnection(this.dbName, false);
} catch (closeError) {
// Ignore close errors - connection might not be properly tracked
logger.debug(
"[CapacitorPlatformService] Error closing connection (may be expected):",
closeError,
);
}
// Now try to create the connection again
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
false,
);
logger.debug(
"[CapacitorPlatformService] Successfully created connection after cleanup",
);
}
} else {
// Re-throw if it's a different error
throw createError;
}
}
// Open the connection if it's not already open
try {
await this.db.open();
} catch (openError: unknown) {
const openErrorMessage =
openError instanceof Error ? openError.message : String(openError);
// If already open, that's fine - continue
if (!openErrorMessage.includes("already open")) {
throw openError;
}
logger.debug(
"[CapacitorPlatformService] Database connection already open",
);
}
// Set journal mode to WAL for better performance // Set journal mode to WAL for better performance
// await this.db.execute("PRAGMA journal_mode=WAL;"); // await this.db.execute("PRAGMA journal_mode=WAL;");

View File

@@ -85,22 +85,12 @@
class="absolute bg-white border border-gray-300 rounded-md mt-1" class="absolute bg-white border border-gray-300 rounded-md mt-1"
> >
<div <div
v-for="methodType in contactMethodTypes"
:key="methodType.value"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer" class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'CELL')" @click="setMethodType(index, methodType.value)"
> >
CELL {{ methodType.label }}
</div>
<div
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'EMAIL')"
>
EMAIL
</div>
<div
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'WHATSAPP')"
>
WHATSAPP
</div> </div>
</div> </div>
</div> </div>
@@ -157,6 +147,7 @@ import {
} from "../constants/notifications"; } from "../constants/notifications";
import { Contact, ContactMethod } from "../db/tables/contacts"; import { Contact, ContactMethod } from "../db/tables/contacts";
import { AppString } from "../constants/app"; import { AppString } from "../constants/app";
import { CONTACT_METHOD_TYPES } from "../constants/contacts";
/** /**
* Contact Edit View Component * Contact Edit View Component
@@ -224,6 +215,8 @@ export default class ContactEditView extends Vue {
/** App string constants */ /** App string constants */
AppString = AppString; AppString = AppString;
/** Contact method types for dropdown */
contactMethodTypes = CONTACT_METHOD_TYPES;
/** /**
* Component lifecycle hook that initializes the contact edit form * Component lifecycle hook that initializes the contact edit form

View File

@@ -20,12 +20,12 @@
</button> </button>
<!-- Help button --> <!-- Help button -->
<button <router-link
:to="{ name: 'help' }"
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full" class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
@click="goToHelp()"
> >
<font-awesome icon="question" class="block text-center w-[1em]" /> <font-awesome icon="question" class="block text-center w-[1em]" />
</button> </router-link>
</div> </div>
<!-- Identity Details --> <!-- Identity Details -->
@@ -42,6 +42,39 @@
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" /> <font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</router-link> </router-link>
</h2> </h2>
<!-- Notes -->
<div v-if="contactFromDid.notes" class="mt-3">
<p class="text-sm text-slate-700 whitespace-pre-wrap">
{{ contactFromDid.notes }}
</p>
</div>
<!-- Contact Methods -->
<div v-if="contactFromDid.contactMethods?.length" class="mt-3">
<div class="flex flex-wrap gap-2">
<div
v-for="(method, index) in contactFromDid.contactMethods"
:key="index"
class="inline-flex items-center gap-2 text-sm"
>
<span class="font-semibold text-slate-600"
>{{ getContactMethodLabel(method.type) }}:</span
>
<span class="text-slate-700">{{ method.label }}</span>
<span class="text-slate-600">{{ method.value }}</span>
<a
v-if="method.type === 'CELL'"
:href="`sms:${method.value}`"
class="ml-2 text-blue-500 hover:text-blue-700"
title="Send text message"
>
<font-awesome icon="message" class="text-base" />
</a>
</div>
</div>
</div>
<button class="ml-2 mr-2 mt-4" @click="toggleDidDetails"> <button class="ml-2 mr-2 mt-4" @click="toggleDidDetails">
Details Details
<font-awesome <font-awesome
@@ -302,6 +335,7 @@ import {
NOTIFY_CONTACT_INVALID_DID, NOTIFY_CONTACT_INVALID_DID,
} from "@/constants/notifications"; } from "@/constants/notifications";
import { THAT_UNNAMED_PERSON } from "@/constants/entities"; import { THAT_UNNAMED_PERSON } from "@/constants/entities";
import { getContactMethodLabel } from "@/constants/contacts";
/** /**
* DIDView Component * DIDView Component
@@ -352,6 +386,7 @@ export default class DIDView extends Vue {
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps; capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
didInfoForContact = didInfoForContact; didInfoForContact = didInfoForContact;
displayAmount = displayAmount; displayAmount = displayAmount;
getContactMethodLabel = getContactMethodLabel;
/** /**
* Initializes notification helpers * Initializes notification helpers

View File

@@ -898,7 +898,13 @@ export default class HomeView extends Vue {
this.starredPlanHandleIds, this.starredPlanHandleIds,
this.lastAckedStarredPlanChangesJwtId, this.lastAckedStarredPlanChangesJwtId,
); );
this.numNewStarredProjectChanges = starredProjectChanges.data.length; // filter out any data elements where there is no wrappedClaimBefore
const filteredNewStarredProjectChanges =
starredProjectChanges.data.filter(
(change) => change.wrappedClaimBefore !== undefined,
);
this.numNewStarredProjectChanges =
filteredNewStarredProjectChanges.length;
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit;
} catch (error) { } catch (error) {
// Don't show errors for starred project changes as it's a secondary feature // Don't show errors for starred project changes as it's a secondary feature

View File

@@ -284,7 +284,10 @@
</table> </table>
</div> </div>
</div> </div>
<div v-else>The changes did not affect essential project data.</div> <div v-else>
The changes are not important, like it was saved by accident or
you've seen it all before.
</div>
<!-- New line that appears on hover --> <!-- New line that appears on hover -->
<div <div
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center" class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@@ -589,13 +592,13 @@ export default class NewActivityView extends Vue {
for (const planChange of planChanges) { for (const planChange of planChanges) {
const currentPlan: PlanSummaryRecord = planChange.plan; const currentPlan: PlanSummaryRecord = planChange.plan;
const wrappedClaim: GenericCredWrapper<PlanActionClaim> = const wrappedClaim: GenericCredWrapper<PlanActionClaim> | undefined =
planChange.wrappedClaimBefore; planChange.wrappedClaimBefore;
// Extract the actual claim from the wrapped claim // Extract the actual claim from the wrapped claim
let previousClaim: PlanActionClaim; let previousClaim: PlanActionClaim | undefined;
const embeddedClaim: PlanActionClaim = wrappedClaim.claim; const embeddedClaim: PlanActionClaim | undefined = wrappedClaim?.claim;
if ( if (
embeddedClaim && embeddedClaim &&
typeof embeddedClaim === "object" && typeof embeddedClaim === "object" &&
@@ -609,7 +612,9 @@ export default class NewActivityView extends Vue {
previousClaim = embeddedClaim; previousClaim = embeddedClaim;
} }
if (!previousClaim || !currentPlan.handleId) { if (!previousClaim) {
// Can happen when a project is starred after the stored last-seen-change-jwt ID
// so we'll just leave the message saying there are no important differences.
continue; continue;
} }

View File

@@ -57,6 +57,9 @@
<button :class="sqlLinkClasses" @click="setAccountsQuery"> <button :class="sqlLinkClasses" @click="setAccountsQuery">
Accounts Accounts
</button> </button>
<button :class="sqlLinkClasses" @click="setActiveIdentityQuery">
Active DID
</button>
<button :class="sqlLinkClasses" @click="setContactsQuery"> <button :class="sqlLinkClasses" @click="setContactsQuery">
Contacts Contacts
</button> </button>
@@ -525,6 +528,11 @@ export default class Help extends Vue {
this.executeSql(); this.executeSql();
} }
setActiveIdentityQuery() {
this.sqlQuery = "SELECT * FROM active_identity;";
this.executeSql();
}
setContactsQuery() { setContactsQuery() {
this.sqlQuery = "SELECT * FROM contacts;"; this.sqlQuery = "SELECT * FROM contacts;";
this.executeSql(); this.executeSql();

View File

@@ -54,6 +54,108 @@
</p> </p>
</div> </div>
<!-- Nearest Neighbors Section -->
<div
v-if="
profile.issuerDid !== activeDid && // only show neighbors if they're not current user
profile.issuerDid !== neighbors?.[0]?.did // and they're not directly connected (since there's no in-between)
"
class="mt-6"
>
<h2 class="text-lg font-semibold mb-3">Network Connections</h2>
<div v-if="loadingNeighbors">
<div class="flex justify-center items-center py-8">
<font-awesome
icon="spinner"
class="fa-spin-pulse text-2xl text-slate-400"
/>
</div>
</div>
<div
v-else-if="neighborsError"
class="bg-red-50 border border-red-300 rounded-md p-4"
>
<div class="flex items-start gap-2">
<font-awesome
icon="exclamation-triangle"
class="text-red-500 mt-0.5"
/>
<p class="text-red-700">{{ neighborsError }}</p>
</div>
</div>
<div v-else>
<div class="space-y-2">
<div
v-for="neighbor in neighbors"
:key="neighbor.did"
class="bg-slate-50 border border-slate-300 rounded-md"
>
<div class="flex items-center justify-between gap-3 p-3">
<div class="flex items-center gap-2 flex-1 min-w-0">
<button
title="Copy profile link and expand"
class="text-blue-600 flex-shrink-0"
@click="onNeighborExpandClick(neighbor.did)"
>
<font-awesome
:icon="
expandedNeighborDid === neighbor.did
? 'chevron-down'
: 'chevron-right'
"
class="text-sm"
/>
{{ getNeighborDisplayName(neighbor.did) }}
</button>
<span :class="getRelationBadgeClass(neighbor.relation)">
{{ getRelationLabel(neighbor.relation) }}
</span>
</div>
</div>
<div
v-if="expandedNeighborDid === neighbor.did"
class="border-t border-slate-300 p-3 bg-white"
>
<router-link
:to="{ name: 'did', params: { did: neighbor.did } }"
class="text-blue-600 hover:text-blue-800 font-medium underline"
>
Go to contact info
</router-link>
and send them the link in your clipboard and ask for an
introduction to this person.
<div
v-if="
getNeighborDisplayName(neighbor.did) === '' ||
neighborIsNotInContacts(neighbor.did)
"
class="flex flex-col gap-1 mt-2"
>
<p class="text-xs text-slate-600">
This person is connected to you, but they are not in this
device's contacts. Copy this DID link and check on another
device or check with different people.
</p>
<span class="flex items-center gap-1 min-w-0">
<span class="text-xs truncate text-slate-600 min-w-0">
{{ neighbor.did }}
</span>
<button
title="Copy DID Link"
class="text-blue-600 hover:text-blue-800 underline cursor-pointer flex-shrink-0"
@click.prevent="onCopyDidClick(neighbor.did)"
>
<font-awesome icon="copy" class="text-sm" />
</button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Map for first coordinates --> <!-- Map for first coordinates -->
<div v-if="hasFirstLocation" class="mt-4"> <div v-if="hasFirstLocation" class="mt-4">
<h2 class="text-lg font-semibold">Location</h2> <h2 class="text-lg font-semibold">Location</h2>
@@ -159,7 +261,11 @@ export default class UserProfileView extends Vue {
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
expandedNeighborDid: string | null = null;
isLoading = true; isLoading = true;
loadingNeighbors = false;
neighbors: Array<{ did: string; relation: string }> = [];
neighborsError = "";
partnerApiServer = DEFAULT_PARTNER_API_SERVER; partnerApiServer = DEFAULT_PARTNER_API_SERVER;
profile: UserProfile | null = null; profile: UserProfile | null = null;
@@ -183,8 +289,8 @@ export default class UserProfileView extends Vue {
*/ */
async mounted() { async mounted() {
await this.initializeSettings(); await this.initializeSettings();
await this.loadContacts();
await this.loadProfile(); await this.loadProfile();
await this.loadNeighbors();
} }
/** /**
@@ -199,12 +305,7 @@ export default class UserProfileView extends Vue {
this.activeDid = activeIdentity.activeDid || ""; this.activeDid = activeIdentity.activeDid || "";
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer; this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
}
/**
* Loads all contacts from database
*/
private async loadContacts() {
this.allContacts = await this.$getAllContacts(); this.allContacts = await this.$getAllContacts();
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
} }
@@ -249,23 +350,100 @@ export default class UserProfileView extends Vue {
} }
/** /**
* Copies profile link to clipboard * Loads nearest neighbors from partner API
* *
* Creates a deep link to the profile and copies it to the clipboard * Fetches network connections for the profile and displays them
* Shows success notification when completed * with appropriate relation labels
*/
async loadNeighbors() {
const profileId: string = this.$route.params.id as string;
if (!profileId) {
return;
}
this.loadingNeighbors = true;
this.neighborsError = "";
try {
const response = await fetch(
`${this.partnerApiServer}/api/partner/userProfileNearestNeighbors/${encodeURIComponent(profileId)}`,
{
method: "GET",
headers: await getHeaders(this.activeDid),
},
);
if (response.status === 200) {
const result = await response.json();
this.neighbors = result.data;
this.neighborsError = "";
} else {
logger.warn("Failed to load neighbors:", response.status);
this.neighbors = [];
this.neighborsError = "Failed to load network connections.";
}
} catch (error) {
logger.error("Error loading neighbors:", error);
this.neighbors = [];
this.neighborsError =
"An error occurred while loading network connections.";
} finally {
this.loadingNeighbors = false;
}
}
/**
* Copies a deep link to the profile to the clipboard
*/ */
async onCopyLinkClick() { async onCopyLinkClick() {
// Use production URL for sharing to avoid localhost issues in development
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`; const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
try { try {
await copyToClipboard(deepLink); await copyToClipboard(deepLink);
this.notify.copied("profile link", TIMEOUTS.STANDARD); this.notify.copied("Profile link", TIMEOUTS.STANDARD);
} catch (error) { } catch (error) {
this.$logAndConsole(`Error copying profile link: ${error}`, true); this.$logAndConsole(`Error copying profile link: ${error}`, true);
this.notify.error("Failed to copy profile link."); this.notify.error("Failed to copy profile link.");
} }
} }
/**
* Copies a deep link to the provided DID to the clipboard
*/
async onCopyDidClick(did: string) {
const deepLink = `${APP_SERVER}/deep-link/did/${encodeURIComponent(did)}`;
try {
await copyToClipboard(deepLink);
this.notify.copied("DID link", TIMEOUTS.STANDARD);
} catch (error) {
this.$logAndConsole(`Error copying DID link: ${error}`, true);
this.notify.error("Failed to copy DID link.");
}
}
/**
* Handles clicking the expand button next to a neighbor's name
* Copies the profile link to clipboard and toggles the expanded section
*/
async onNeighborExpandClick(did: string) {
if (this.expandedNeighborDid === did) {
this.expandedNeighborDid = null;
// don't copy the link
return;
}
// Copy the profile link
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
try {
await copyToClipboard(deepLink);
this.notify.copied("Profile link", TIMEOUTS.STANDARD);
} catch (error) {
this.$logAndConsole(`Error copying profile link: ${error}`, true);
this.notify.error("Failed to copy profile link.");
}
// Toggle the expanded section
this.expandedNeighborDid = did;
}
/** /**
* Computed properties for template logic streamlining * Computed properties for template logic streamlining
*/ */
@@ -330,5 +508,64 @@ export default class UserProfileView extends Vue {
get tileLayerUrl() { get tileLayerUrl() {
return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; return "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
} }
/**
* Gets display name for a neighbor's DID
* Uses didInfo utility to show contact name if available, otherwise DID
* @param did - The DID to get display name for
* @returns Formatted display name
*/
getNeighborDisplayName(did: string): string {
return this.didInfo(did, this.activeDid, this.allMyDids, this.allContacts);
}
neighborIsNotInContacts(did: string) {
return !this.allContacts.some((contact) => contact.did === did);
}
noNeighborsAreInContacts() {
return this.neighbors.every(
(neighbor) =>
!this.allContacts.some((contact) => contact.did === neighbor.did),
);
}
/**
* Gets human-readable label for relation type
* @param relation - The relation type from API
* @returns Display label for the relation
*/
getRelationLabel(relation: string): string {
switch (relation) {
case "REGISTERED_BY_YOU":
return "Registered by You";
case "REGISTERED_YOU":
return "Registered You";
case "TARGET":
return "Yourself";
default:
return relation;
}
}
/**
* Gets CSS classes for relation badge styling
* @param relation - The relation type from API
* @returns CSS class string for badge
*/
getRelationBadgeClass(relation: string): string {
const baseClasses =
"text-xs font-semibold px-2 py-1 rounded whitespace-nowrap";
switch (relation) {
case "REGISTERED_BY_YOU":
return `${baseClasses} bg-blue-100 text-blue-700`;
case "REGISTERED_YOU":
return `${baseClasses} bg-green-100 text-green-700`;
case "TARGET":
return `${baseClasses} bg-purple-100 text-purple-700`;
default:
return `${baseClasses} bg-slate-100 text-slate-700`;
}
}
} }
</script> </script>