Compare commits
14 Commits
entitygrid
...
accountvie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36eb9a16b0 | ||
|
|
203cf6b078 | ||
|
|
9b84b28a78 | ||
| 7abce8f95c | |||
| 88dce4d100 | |||
| 06fdaff879 | |||
| 8024a3d02a | |||
| 83b470e28a | |||
| 1739567b18 | |||
| 5050156beb | |||
| d265a9f78c | |||
| f848de15f1 | |||
| ebaf2dedf0 | |||
|
|
749204f96b |
57
BUILDING.md
57
BUILDING.md
@@ -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:
|
||||
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$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;
|
||||
|
||||
```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 -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
```
|
||||
```bash
|
||||
cd ios/App && xcrun agvtool new-version 47 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.1.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
```
|
||||
|
||||
##### 2. Build
|
||||
|
||||
Here's prod. Also available: test, dev
|
||||
Here's prod. Also available: test, dev
|
||||
|
||||
```bash
|
||||
npm run build:ios:prod
|
||||
```
|
||||
```bash
|
||||
npm run build:ios:prod
|
||||
```
|
||||
|
||||
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.
|
||||
- You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||
- 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
|
||||
|
||||
@@ -1315,26 +1316,26 @@ The recommended way to build for Android is using the automated build script:
|
||||
|
||||
#### 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
|
||||
perl -p -i -e 's/versionCode .*/versionCode 46/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.1.1"/g' android/app/build.gradle
|
||||
```
|
||||
```bash
|
||||
perl -p -i -e 's/versionCode .*/versionCode 47/g' android/app/build.gradle
|
||||
perl -p -i -e 's/versionName .*/versionName "1.1.2"/g' android/app/build.gradle
|
||||
```
|
||||
|
||||
##### 2. Build
|
||||
|
||||
Here's prod. Also available: test, dev
|
||||
|
||||
```bash
|
||||
npm run build:android:prod
|
||||
```
|
||||
```bash
|
||||
npm run build:android:prod
|
||||
```
|
||||
|
||||
##### 3. Open the project in Android Studio
|
||||
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
```bash
|
||||
npx cap open android
|
||||
```
|
||||
|
||||
##### 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
|
||||
those changes or your (closed) testers won't see it.
|
||||
|
||||
- When finished, bump package.json version
|
||||
|
||||
### Capacitor Operations
|
||||
|
||||
```bash
|
||||
|
||||
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [1.1.2] - 2025.11.06
|
||||
|
||||
### Fixed
|
||||
- Bad page when user follows prompt to backup seed
|
||||
|
||||
|
||||
## [1.1.1] - 2025.11.03
|
||||
|
||||
### Added
|
||||
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 46
|
||||
versionName "1.1.1"
|
||||
versionCode 47
|
||||
versionName "1.1.2"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -413,7 +413,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.1.2;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -430,7 +430,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
CURRENT_PROJECT_VERSION = 47;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -440,7 +440,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.1;
|
||||
MARKETING_VERSION = 1.1.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.1.2-beta",
|
||||
"version": "1.1.3-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "timesafari",
|
||||
"version": "1.1.2-beta",
|
||||
"version": "1.1.3-beta",
|
||||
"dependencies": {
|
||||
"@capacitor-community/electron": "^5.0.1",
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "1.1.2-beta",
|
||||
"version": "1.1.3-beta",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
|
||||
@@ -436,7 +436,21 @@ fi
|
||||
log_info "Cleaning dist directory..."
|
||||
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
|
||||
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
|
||||
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
|
||||
fi
|
||||
|
||||
# Step 5: Clean Gradle build
|
||||
# Step 6: Clean Gradle build
|
||||
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
|
||||
safe_execute "Assembling debug build" "cd android && ./gradlew assembleDebug && cd .." || exit 5
|
||||
elif [ "$BUILD_TYPE" = "release" ]; then
|
||||
safe_execute "Assembling release build" "cd android && ./gradlew assembleRelease && cd .." || exit 5
|
||||
fi
|
||||
|
||||
# Step 7: Sync with Capacitor
|
||||
# Step 8: Sync with Capacitor
|
||||
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
|
||||
|
||||
# Step 9: Build APK/AAB if requested
|
||||
# Step 10: Build APK/AAB if requested
|
||||
if [ "$BUILD_APK" = true ]; then
|
||||
if [ "$BUILD_TYPE" = "debug" ]; then
|
||||
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
|
||||
fi
|
||||
|
||||
# Step 10: Auto-run app if requested
|
||||
# Step 11: Auto-run app if requested
|
||||
if [ "$AUTO_RUN" = true ]; then
|
||||
log_step "Auto-running Android app..."
|
||||
safe_execute "Launching app" "npx cap run android" || {
|
||||
@@ -485,7 +499,7 @@ if [ "$AUTO_RUN" = true ]; then
|
||||
log_success "Android app launched successfully!"
|
||||
fi
|
||||
|
||||
# Step 11: Open Android Studio if requested
|
||||
# Step 12: Open Android Studio if requested
|
||||
if [ "$OPEN_STUDIO" = true ]; then
|
||||
safe_execute "Opening Android Studio" "npx cap open android" || exit 8
|
||||
fi
|
||||
|
||||
@@ -381,7 +381,21 @@ safe_execute "Cleaning iOS build" "clean_ios_build" || exit 1
|
||||
log_info "Cleaning dist directory..."
|
||||
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
|
||||
safe_execute "Building Capacitor version (development)" "npm run build:capacitor" || exit 3
|
||||
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
|
||||
fi
|
||||
|
||||
# Step 5: Sync with Capacitor
|
||||
# Step 6: Sync with Capacitor
|
||||
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
|
||||
|
||||
# Step 7: Build iOS app
|
||||
# Step 8: Build iOS app
|
||||
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
|
||||
log_info "Building IPA package..."
|
||||
cd ios/App
|
||||
@@ -426,12 +440,12 @@ if [ "$BUILD_APP" = true ]; then
|
||||
log_success "App bundle built successfully"
|
||||
fi
|
||||
|
||||
# Step 9: Auto-run app if requested
|
||||
# Step 10: Auto-run app if requested
|
||||
if [ "$AUTO_RUN" = true ]; then
|
||||
safe_execute "Auto-running iOS app" "auto_run_ios_app" || exit 9
|
||||
fi
|
||||
|
||||
# Step 10: Open Xcode if requested
|
||||
# Step 11: Open Xcode if requested
|
||||
if [ "$OPEN_STUDIO" = true ]; then
|
||||
safe_execute "Opening Xcode" "npx cap open ios" || exit 8
|
||||
fi
|
||||
|
||||
@@ -10,7 +10,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
|
||||
<template>
|
||||
<div id="sectionDataExport" :class="containerClasses">
|
||||
<div :class="titleClasses">Data Export</div>
|
||||
<div :class="titleClasses">Data Management</div>
|
||||
<router-link
|
||||
v-if="activeDid"
|
||||
:to="{ name: 'seed-backup' }"
|
||||
@@ -30,7 +30,7 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
:class="exportButtonClasses"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
{{ isExporting ? "Exporting..." : "Download Contacts" }}
|
||||
{{ isExporting ? "Exporting..." : "Export Contacts" }}
|
||||
</button>
|
||||
|
||||
<div
|
||||
@@ -55,11 +55,54 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Import Contacts -->
|
||||
<div id="sectionImportContactsSettings" class="mt-4">
|
||||
<h2 class="text-slate-500 text-sm font-bold">Import Contacts</h2>
|
||||
|
||||
<div class="mt-2">
|
||||
<input
|
||||
type="file"
|
||||
class="w-full bg-white rounded-md pe-2 file:border-0 file:bg-gradient-to-b file:from-blue-400 file:to-blue-700 file:shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] file:text-white file:px-3 file:py-2 file:me-2 file:rounded-s-md"
|
||||
@change="uploadImportFile"
|
||||
/>
|
||||
<transition
|
||||
enter-active-class="transform ease-out duration-300 transition"
|
||||
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
|
||||
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
|
||||
leave-active-class="transition ease-in duration-500"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="showContactImport()" class="mt-2">
|
||||
<!-- Bulk import has an error
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||
@click="confirmSubmitImportFile()"
|
||||
>
|
||||
Overwrite Settings & Contacts
|
||||
<br />
|
||||
(which doesn't include Identifier Data)
|
||||
</button>
|
||||
</div>
|
||||
-->
|
||||
<button
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||
@click="checkContactImports()"
|
||||
>
|
||||
Import Contacts
|
||||
</button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
import * as R from "ramda";
|
||||
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
@@ -67,8 +110,10 @@ import { Contact } from "../db/tables/contacts";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { contactsToExportJson } from "../libs/util";
|
||||
import { createNotifyHelpers } from "@/utils/notify";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { ImportContent } from "@/interfaces/accountView";
|
||||
|
||||
/**
|
||||
* @vue-component
|
||||
@@ -91,6 +136,12 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
/**
|
||||
* Router instance injected by Vue
|
||||
* Used for navigation
|
||||
*/
|
||||
$router!: Router;
|
||||
|
||||
/**
|
||||
* Active DID (Decentralized Identifier) of the user
|
||||
* Controls visibility of seed backup option
|
||||
@@ -110,6 +161,12 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
showRedNotificationDot = false;
|
||||
|
||||
/**
|
||||
* Reference to the selected import file
|
||||
* Used to store the file selected by the user for import
|
||||
*/
|
||||
private inputImportFileName: Blob | undefined;
|
||||
|
||||
/**
|
||||
* Notification helper for consistent notification patterns
|
||||
* Created as a getter to ensure $notify is available when called
|
||||
@@ -200,12 +257,30 @@ export default class DataExportSection extends Vue {
|
||||
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
|
||||
const exContact: Contact = R.omit(["contactMethods"], contact);
|
||||
// now add contactMethods as a true array of ContactMethod objects
|
||||
exContact.contactMethods = contact.contactMethods
|
||||
? typeof contact.contactMethods === "string" &&
|
||||
contact.contactMethods.trim() !== ""
|
||||
? JSON.parse(contact.contactMethods)
|
||||
: []
|
||||
: [];
|
||||
// $contacts() returns normalized contacts where contactMethods is already an array,
|
||||
// but we handle both array and string cases for robustness
|
||||
if (contact.contactMethods) {
|
||||
if (Array.isArray(contact.contactMethods)) {
|
||||
// Already an array, use it directly
|
||||
exContact.contactMethods = contact.contactMethods;
|
||||
} else {
|
||||
// Check if it's a string that needs parsing (shouldn't happen with normalized contacts, but handle for robustness)
|
||||
const contactMethodsValue = contact.contactMethods as unknown;
|
||||
if (
|
||||
typeof contactMethodsValue === "string" &&
|
||||
contactMethodsValue.trim() !== ""
|
||||
) {
|
||||
// String that needs parsing
|
||||
exContact.contactMethods = JSON.parse(contactMethodsValue);
|
||||
} else {
|
||||
// Invalid data, use empty array
|
||||
exContact.contactMethods = [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No contactMethods, use empty array
|
||||
exContact.contactMethods = [];
|
||||
}
|
||||
return exContact;
|
||||
});
|
||||
|
||||
@@ -248,5 +323,58 @@ export default class DataExportSection extends Vue {
|
||||
this.showRedNotificationDot = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles file selection for contact import
|
||||
* Stores the selected file for later processing
|
||||
*/
|
||||
async uploadImportFile(event: Event): Promise<void> {
|
||||
this.inputImportFileName = (event.target as HTMLInputElement).files?.[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a contact import file has been selected
|
||||
* Used to conditionally show the import button
|
||||
*/
|
||||
showContactImport(): boolean {
|
||||
return !!this.inputImportFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the selected import file and navigates to the contact import view
|
||||
* Parses the JSON file and extracts contact data for import
|
||||
*/
|
||||
async checkContactImports(): Promise<void> {
|
||||
if (!this.inputImportFileName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const fileContent: string = (event.target?.result as string) || "{}";
|
||||
try {
|
||||
const contents: ImportContent = JSON.parse(fileContent);
|
||||
const contactTableRows: Array<Contact> = (
|
||||
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
|
||||
)?.find((table) => table.tableName === "contacts")
|
||||
?.rows as Array<Contact>;
|
||||
const contactRows = contactTableRows.map(
|
||||
// @ts-expect-error for omitting this field that is found in the Dexie format
|
||||
(contact) => R.omit(["$types"], contact) as Contact,
|
||||
);
|
||||
this.$router.push({
|
||||
name: "contact-import",
|
||||
query: { contacts: JSON.stringify(contactRows) },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error checking contact imports:", error);
|
||||
this.notify.error(
|
||||
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMPORT_ERROR,
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(this.inputImportFileName);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -57,7 +57,12 @@ export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
|
||||
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 {
|
||||
agentDid?: string;
|
||||
description: string;
|
||||
@@ -76,7 +81,9 @@ export interface PlanSummaryRecord {
|
||||
|
||||
export interface PlanSummaryAndPreviousClaim {
|
||||
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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1686,7 +1686,10 @@ export async function register(
|
||||
"Registration thrown error:",
|
||||
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." };
|
||||
}
|
||||
|
||||
@@ -91,16 +91,92 @@ export class CapacitorPlatformService
|
||||
}
|
||||
|
||||
try {
|
||||
// Create/Open database
|
||||
this.db = await this.sqlite.createConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
"no-encryption",
|
||||
1,
|
||||
false,
|
||||
);
|
||||
// Try to create/Open database connection
|
||||
try {
|
||||
this.db = await this.sqlite.createConnection(
|
||||
this.dbName,
|
||||
false,
|
||||
"no-encryption",
|
||||
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
|
||||
// await this.db.execute("PRAGMA journal_mode=WAL;");
|
||||
|
||||
@@ -1367,6 +1367,9 @@ export const PlatformServiceMixin = {
|
||||
contact.profileImageUrl !== undefined
|
||||
? contact.profileImageUrl
|
||||
: null,
|
||||
notes: contact.notes !== undefined ? contact.notes : null,
|
||||
iViewContent:
|
||||
contact.iViewContent !== undefined ? contact.iViewContent : null,
|
||||
contactMethods:
|
||||
contact.contactMethods !== undefined
|
||||
? Array.isArray(contact.contactMethods)
|
||||
@@ -1377,8 +1380,8 @@ export const PlatformServiceMixin = {
|
||||
|
||||
await this.$dbExec(
|
||||
`INSERT OR REPLACE INTO contacts
|
||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, contactMethods)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
(did, name, publicKeyBase64, seesMe, registered, nextPubKeyHashB64, profileImageUrl, notes, iViewContent, contactMethods)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
safeContact.did,
|
||||
safeContact.name,
|
||||
@@ -1387,6 +1390,8 @@ export const PlatformServiceMixin = {
|
||||
safeContact.registered,
|
||||
safeContact.nextPubKeyHashB64,
|
||||
safeContact.profileImageUrl,
|
||||
safeContact.notes,
|
||||
safeContact.iViewContent,
|
||||
safeContact.contactMethods,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -375,45 +375,6 @@
|
||||
Switch Identifier
|
||||
</router-link>
|
||||
|
||||
<div id="sectionImportContactsSettings" class="mt-4">
|
||||
<h2 class="text-slate-500 text-sm font-bold">Import Contacts</h2>
|
||||
|
||||
<div class="ml-4 mt-2">
|
||||
<input type="file" class="ml-2" @change="uploadImportFile" />
|
||||
<transition
|
||||
enter-active-class="transform ease-out duration-300 transition"
|
||||
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
|
||||
enter-to-class="translate-y-0 opacity-100 sm:translate-y-0"
|
||||
leave-active-class="transition ease-in duration-500"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div v-if="showContactImport()" class="mt-4">
|
||||
<!-- Bulk import has an error
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||
@click="confirmSubmitImportFile()"
|
||||
>
|
||||
Overwrite Settings & Contacts
|
||||
<br />
|
||||
(which doesn't include Identifier Data)
|
||||
</button>
|
||||
</div>
|
||||
-->
|
||||
<div class="flex justify-center">
|
||||
<button
|
||||
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
||||
@click="checkContactImports()"
|
||||
>
|
||||
Import Contacts
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label
|
||||
for="toggleShowAmounts"
|
||||
class="flex items-center justify-between cursor-pointer my-4"
|
||||
@@ -770,9 +731,7 @@ import "dexie-export-import";
|
||||
import { ImportProgress } from "dexie-export-import";
|
||||
import { LeafletMouseEvent } from "leaflet";
|
||||
import * as L from "leaflet";
|
||||
import * as R from "ramda";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import { copyToClipboard } from "../services/ClipboardService";
|
||||
@@ -799,7 +758,6 @@ import {
|
||||
NotificationIface,
|
||||
PASSKEYS_ENABLED,
|
||||
} from "../constants/app";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||
BoundingBox,
|
||||
@@ -823,11 +781,7 @@ import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
||||
import {
|
||||
AccountSettings,
|
||||
isApiError,
|
||||
ImportContent,
|
||||
} from "@/interfaces/accountView";
|
||||
import { AccountSettings, isApiError } from "@/interfaces/accountView";
|
||||
// Profile data interface (inlined from ProfileService)
|
||||
interface ProfileData {
|
||||
description: string;
|
||||
@@ -836,8 +790,6 @@ interface ProfileData {
|
||||
includeLocation: boolean;
|
||||
}
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
interface UserNameDialogRef {
|
||||
open: (cb: (name?: string) => void) => void;
|
||||
}
|
||||
@@ -1369,65 +1321,6 @@ export default class AccountViewView extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
async uploadImportFile(event: Event): Promise<void> {
|
||||
inputImportFileNameRef.value = (
|
||||
event.target as HTMLInputElement
|
||||
).files?.[0];
|
||||
}
|
||||
|
||||
showContactImport(): boolean {
|
||||
return !!inputImportFileNameRef.value;
|
||||
}
|
||||
|
||||
confirmSubmitImportFile(): void {
|
||||
if (inputImportFileNameRef.value != null) {
|
||||
this.notify.confirm(
|
||||
ACCOUNT_VIEW_CONSTANTS.WARNINGS.IMPORT_REPLACE_WARNING,
|
||||
this.submitImportFile,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously imports the database from a downloadable JSON file.
|
||||
*
|
||||
* @throws Will notify the user if there is an export error.
|
||||
*/
|
||||
async submitImportFile(): Promise<void> {
|
||||
if (inputImportFileNameRef.value != null) {
|
||||
// TODO: implement this for SQLite
|
||||
}
|
||||
}
|
||||
|
||||
async checkContactImports(): Promise<void> {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const fileContent: string = (event.target?.result as string) || "{}";
|
||||
try {
|
||||
const contents: ImportContent = JSON.parse(fileContent);
|
||||
const contactTableRows: Array<Contact> = (
|
||||
contents.data?.data as [{ tableName: string; rows: Array<Contact> }]
|
||||
)?.find((table) => table.tableName === "contacts")
|
||||
?.rows as Array<Contact>;
|
||||
const contactRows = contactTableRows.map(
|
||||
// @ts-expect-error for omitting this field that is found in the Dexie format
|
||||
(contact) => R.omit(["$types"], contact) as Contact,
|
||||
);
|
||||
(this.$router as Router).push({
|
||||
name: "contact-import",
|
||||
query: { contacts: JSON.stringify(contactRows) },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error checking contact imports:", error);
|
||||
this.notify.error(
|
||||
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMPORT_ERROR,
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
}
|
||||
};
|
||||
reader.readAsText(inputImportFileNameRef.value as Blob);
|
||||
}
|
||||
|
||||
private progressCallback(progress: ImportProgress): boolean {
|
||||
logger.log(
|
||||
`Import progress: ${progress.completedRows} of ${progress.totalRows} rows completed.`,
|
||||
|
||||
@@ -338,9 +338,10 @@ export default class ContactEditView extends Vue {
|
||||
}
|
||||
|
||||
// Save to database via PlatformServiceMixin
|
||||
// Normalize empty strings to null to preserve database consistency
|
||||
await this.$updateContact(this.contact?.did || "", {
|
||||
name: this.contactName,
|
||||
notes: this.contactNotes,
|
||||
name: this.contactName?.trim() || null,
|
||||
notes: this.contactNotes?.trim() || null,
|
||||
contactMethods: contactMethods,
|
||||
});
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
</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"
|
||||
@click="goToHelp()"
|
||||
>
|
||||
<font-awesome icon="question" class="block text-center w-[1em]" />
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Identity Details -->
|
||||
|
||||
@@ -898,7 +898,13 @@ export default class HomeView extends Vue {
|
||||
this.starredPlanHandleIds,
|
||||
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;
|
||||
} catch (error) {
|
||||
// Don't show errors for starred project changes as it's a secondary feature
|
||||
|
||||
@@ -284,7 +284,10 @@
|
||||
</table>
|
||||
</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 -->
|
||||
<div
|
||||
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) {
|
||||
const currentPlan: PlanSummaryRecord = planChange.plan;
|
||||
const wrappedClaim: GenericCredWrapper<PlanActionClaim> =
|
||||
const wrappedClaim: GenericCredWrapper<PlanActionClaim> | undefined =
|
||||
planChange.wrappedClaimBefore;
|
||||
|
||||
// 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 (
|
||||
embeddedClaim &&
|
||||
typeof embeddedClaim === "object" &&
|
||||
@@ -609,7 +612,9 @@ export default class NewActivityView extends Vue {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
<button :class="sqlLinkClasses" @click="setAccountsQuery">
|
||||
Accounts
|
||||
</button>
|
||||
<button :class="sqlLinkClasses" @click="setActiveIdentityQuery">
|
||||
Active DID
|
||||
</button>
|
||||
<button :class="sqlLinkClasses" @click="setContactsQuery">
|
||||
Contacts
|
||||
</button>
|
||||
@@ -525,6 +528,11 @@ export default class Help extends Vue {
|
||||
this.executeSql();
|
||||
}
|
||||
|
||||
setActiveIdentityQuery() {
|
||||
this.sqlQuery = "SELECT * FROM active_identity;";
|
||||
this.executeSql();
|
||||
}
|
||||
|
||||
setContactsQuery() {
|
||||
this.sqlQuery = "SELECT * FROM contacts;";
|
||||
this.executeSql();
|
||||
|
||||
Reference in New Issue
Block a user