Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19c9609859 | |||
| a96cc8155c | |||
| 1b283a0045 | |||
| afd407e178 | |||
|
|
59b13823c8 | ||
| 3baa6633a6 | |||
| bda98eb632 | |||
| eea1cb995a | |||
| 276e0a741b | |||
| e46d6133fb | |||
| 94994a7251 | |||
| 838723c26b | |||
| bb6eb92ba1 | |||
| a997d4cb92 | |||
| 73733345ff | |||
| 5aa693de63 | |||
| 6f2272eea7 | |||
| 3118f71320 | |||
| d12f23aa81 | |||
| e9a8a3c1e7 | |||
| 1e0efe6011 | |||
| 16557f1e4b | |||
| c4a54967bc | |||
| 20ade415dc | |||
| 6689520270 | |||
| 3fd6c2b80d |
18
BUILDING.md
18
BUILDING.md
@@ -41,6 +41,7 @@ Install dependencies:
|
|||||||
1. Run the production build:
|
1. Run the production build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
rm -rf dist
|
||||||
npm run build:web
|
npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -62,11 +63,13 @@ Install dependencies:
|
|||||||
|
|
||||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||||
|
|
||||||
|
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
|
||||||
|
|
||||||
* Commit everything (since the commit hash is used the app).
|
* Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
* Put the commit hash in the changelog (which will help you remember to bump the version in the step later).
|
||||||
|
|
||||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.5.8 && git push origin 0.5.8`.
|
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.2 && git push origin 1.0.2`.
|
||||||
|
|
||||||
* For test, build the app (because test server is not yet set up to build):
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
@@ -90,13 +93,13 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
|
|||||||
|
|
||||||
* `pkgx +npm sh`
|
* `pkgx +npm sh`
|
||||||
|
|
||||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.8 && npm install && npm run build:web && cd -`
|
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web && cd -`
|
||||||
|
|
||||||
(The plain `npm run build:web` uses the .env.production file.)
|
(The plain `npm run build:web` uses the .env.production file.)
|
||||||
|
|
||||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-2 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||||
|
|
||||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production.
|
||||||
|
|
||||||
## Docker Deployment
|
## Docker Deployment
|
||||||
|
|
||||||
@@ -359,12 +362,9 @@ Prerequisites: macOS with Xcode installed
|
|||||||
4. Bump the version to match Android & package.json:
|
4. Bump the version to match Android & package.json:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App
|
cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||||
xcrun agvtool new-version 33
|
|
||||||
# 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.7;/g" > temp && mv temp App.xcodeproj/project.pbxproj
|
|
||||||
cd -
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Open the project in Xcode:
|
5. Open the project in Xcode:
|
||||||
|
|||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -6,10 +6,32 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- Photo is pinned to profile mode
|
||||||
|
|
||||||
## [0.5.9]
|
|
||||||
|
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
||||||
### Added
|
### Added
|
||||||
- Migration from IndexedDB to SQLite
|
- Version on feed title
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.1] - 2025.06.20
|
||||||
|
### Added
|
||||||
|
- Allow a user to block someone else's content from view
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73
|
||||||
|
### Added
|
||||||
|
- Web-oriented migration from IndexedDB to SQLite
|
||||||
|
|
||||||
|
|
||||||
|
## [0.5.8]
|
||||||
|
### Added
|
||||||
|
- /deep-link/ path for URLs that are shared with people
|
||||||
|
### Changed
|
||||||
|
- External links now go to /deep-link/...
|
||||||
|
- Feed visuals now have arrow imagery from giver to receiver
|
||||||
|
|
||||||
|
|
||||||
## [0.4.7]
|
## [0.4.7]
|
||||||
|
|||||||
@@ -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 33
|
versionCode 35
|
||||||
versionName "0.5.7"
|
versionName "1.0.2"
|
||||||
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.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/title_activity_main"
|
android:label="@string/title_activity_main"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
android:theme="@style/AppTheme.NoActionBarLaunch">
|
android:theme="@style/AppTheme.NoActionBarLaunch">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ try {
|
|||||||
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
||||||
- `src/services/deepLinks.ts`: Deep link processing service
|
- `src/services/deepLinks.ts`: Deep link processing service
|
||||||
- `src/main.capacitor.ts`: Capacitor integration
|
- `src/main.capacitor.ts`: Capacitor integration
|
||||||
|
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||||
|
|
||||||
## Type Safety Examples
|
## Type Safety Examples
|
||||||
|
|
||||||
|
|||||||
@@ -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 = 33;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
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.7;
|
MARKETING_VERSION = 1.0.2;
|
||||||
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 = 33;
|
CURRENT_PROJECT_VERSION = 35;
|
||||||
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.7;
|
MARKETING_VERSION = 1.0.2;
|
||||||
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 = "";
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.5.9",
|
"version": "1.0.3-beta",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.5.9",
|
"version": "1.0.3-beta",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "6.0.2",
|
||||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.5.9",
|
"version": "1.0.3-beta",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
|
|||||||
@@ -36,19 +36,12 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
trace: "retain-on-failure",
|
trace: "retain-on-failure",
|
||||||
|
|
||||||
// Add request logging
|
|
||||||
logger: {
|
|
||||||
isEnabled: (name, severity) => severity === 'error' || name === 'api',
|
|
||||||
log: (name, severity, message, args) => console.log(`${severity}: ${message}`, args)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
// {
|
// {
|
||||||
// name: 'chromium-serial',
|
// name: 'chromium-serial',
|
||||||
// testMatch: /.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts/,
|
|
||||||
// use: {
|
// use: {
|
||||||
// ...devices['Desktop Chrome'],
|
// ...devices['Desktop Chrome'],
|
||||||
// permissions: ["clipboard-read"],
|
// permissions: ["clipboard-read"],
|
||||||
@@ -57,13 +50,11 @@ export default defineConfig({
|
|||||||
// },
|
// },
|
||||||
// {
|
// {
|
||||||
// name: 'firefox-serial',
|
// name: 'firefox-serial',
|
||||||
// testMatch: /.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts/,
|
|
||||||
// use: { ...devices['Desktop Firefox'] },
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
// workers: 1,
|
// workers: 1,
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
|
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Chrome'],
|
...devices['Desktop Chrome'],
|
||||||
permissions: ["clipboard-read"],
|
permissions: ["clipboard-read"],
|
||||||
@@ -71,7 +62,6 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'firefox',
|
name: 'firefox',
|
||||||
testMatch: /^(?!.*\/(35-record-gift-from-image-share|40-add-contact)\.spec\.ts).+\.spec\.ts$/,
|
|
||||||
use: { ...devices['Desktop Firefox'] },
|
use: { ...devices['Desktop Firefox'] },
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
||||||
>click here to copy this page, paste it into a message, and ask if
|
>click here to copy this page, paste it into a message, and ask if
|
||||||
they'll tell you more about the {{ roleName }}.</a
|
they'll tell you more about the {{ roleName }}.</a
|
||||||
>
|
>
|
||||||
@@ -104,7 +104,7 @@ import * as R from "ramda";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class HiddenDidDialog extends Vue {
|
export default class HiddenDidDialog extends Vue {
|
||||||
@@ -117,7 +117,8 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
canShare = false;
|
canShare = false;
|
||||||
windowLocation = window.location.href;
|
deepLinkPathSuffix = "";
|
||||||
|
deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
|
||||||
|
|
||||||
R = R;
|
R = R;
|
||||||
serverUtil = serverUtil;
|
serverUtil = serverUtil;
|
||||||
@@ -129,17 +130,21 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(
|
open(
|
||||||
|
deepLinkPathSuffix: string,
|
||||||
roleName: string,
|
roleName: string,
|
||||||
visibleToDids: string[],
|
visibleToDids: string[],
|
||||||
allContacts: Array<Contact>,
|
allContacts: Array<Contact>,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
allMyDids: Array<string>,
|
allMyDids: Array<string>,
|
||||||
) {
|
) {
|
||||||
|
this.deepLinkPathSuffix = deepLinkPathSuffix;
|
||||||
this.roleName = roleName;
|
this.roleName = roleName;
|
||||||
this.visibleToDids = visibleToDids;
|
this.visibleToDids = visibleToDids;
|
||||||
this.allContacts = allContacts;
|
this.allContacts = allContacts;
|
||||||
this.activeDid = activeDid;
|
this.activeDid = activeDid;
|
||||||
this.allMyDids = allMyDids;
|
this.allMyDids = allMyDids;
|
||||||
|
|
||||||
|
this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +178,11 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.deepLinkUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const secretBase64 = arrayBufferToBase64(randomBytes);
|
|||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
{
|
{
|
||||||
name: "001_initial",
|
name: "001_initial",
|
||||||
// see ../db/tables files for explanations of the fields
|
|
||||||
sql: `
|
sql: `
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -119,6 +118,12 @@ const MIGRATIONS = [
|
|||||||
);
|
);
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "002_add_iViewContent_to_contacts",
|
||||||
|
sql: `
|
||||||
|
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -219,9 +219,9 @@ export async function logConsoleAndDb(
|
|||||||
isError = false,
|
isError = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (isError) {
|
if (isError) {
|
||||||
logger.error(`${new Date().toISOString()} ${message}`);
|
logger.error(`${new Date().toISOString()}`, message);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${new Date().toISOString()} ${message}`);
|
logger.log(`${new Date().toISOString()}`, message);
|
||||||
}
|
}
|
||||||
await logToDb(message);
|
await logToDb(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
export interface ContactMethod {
|
export type ContactMethod = {
|
||||||
label: string;
|
label: string;
|
||||||
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
|
||||||
value: string;
|
value: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface Contact {
|
export type Contact = {
|
||||||
//
|
//
|
||||||
// When adding a property, consider whether it should be added when exporting & sharing contacts.
|
// When adding a property, consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
|
||||||
|
|
||||||
did: string;
|
did: string;
|
||||||
contactMethods?: Array<ContactMethod>;
|
contactMethods?: Array<ContactMethod>;
|
||||||
|
iViewContent?: boolean;
|
||||||
name?: string;
|
name?: string;
|
||||||
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
|
||||||
notes?: string;
|
notes?: string;
|
||||||
@@ -17,9 +18,15 @@ export interface Contact {
|
|||||||
publicKeyBase64?: string;
|
publicKeyBase64?: string;
|
||||||
seesMe?: boolean; // cached value of the server setting
|
seesMe?: boolean; // cached value of the server setting
|
||||||
registered?: boolean; // cached value of the server setting
|
registered?: boolean; // cached value of the server setting
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ContactWithJsonStrings = Contact & {
|
/**
|
||||||
|
* This is for those cases (eg. with a DB) where every field is a primitive (and not an object).
|
||||||
|
*
|
||||||
|
* This is so that we can reuse most of the type and don't have to maintain another copy.
|
||||||
|
* Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
|
||||||
|
*/
|
||||||
|
export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
|
||||||
contactMethods?: string;
|
contactMethods?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,17 @@ 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",
|
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
||||||
"project",
|
|
||||||
"onboard-meeting-setup",
|
|
||||||
"invite-one-accept",
|
|
||||||
"contact-import",
|
|
||||||
"confirm-gift",
|
|
||||||
"claim",
|
"claim",
|
||||||
"claim-cert",
|
|
||||||
"claim-add-raw",
|
"claim-add-raw",
|
||||||
"contact-edit",
|
"claim-cert",
|
||||||
"contacts",
|
"confirm-gift",
|
||||||
|
"contact-import",
|
||||||
"did",
|
"did",
|
||||||
|
"invite-one-accept",
|
||||||
|
"onboard-meeting-setup",
|
||||||
|
"project",
|
||||||
|
"user-profile",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Create a type from the array
|
// Create a type from the array
|
||||||
@@ -58,44 +57,39 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
|||||||
|
|
||||||
// Parameter validation schemas for each route type
|
// Parameter validation schemas for each route type
|
||||||
export const deepLinkSchemas = {
|
export const deepLinkSchemas = {
|
||||||
"user-profile": z.object({
|
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
project: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"onboard-meeting-setup": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"invite-one-accept": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"contact-import": z.object({
|
|
||||||
jwt: z.string(),
|
|
||||||
}),
|
|
||||||
"confirm-gift": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
claim: z.object({
|
claim: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
"claim-cert": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"claim-add-raw": z.object({
|
"claim-add-raw": z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
claim: z.string().optional(),
|
claim: z.string().optional(),
|
||||||
claimJwtId: z.string().optional(),
|
claimJwtId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
"contact-edit": z.object({
|
"claim-cert": z.object({
|
||||||
did: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
contacts: z.object({
|
"confirm-gift": z.object({
|
||||||
contacts: z.string(), // JSON string of contacts array
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"contact-import": z.object({
|
||||||
|
jwt: z.string(),
|
||||||
}),
|
}),
|
||||||
did: z.object({
|
did: z.object({
|
||||||
did: z.string(),
|
did: z.string(),
|
||||||
}),
|
}),
|
||||||
|
"invite-one-accept": z.object({
|
||||||
|
jwt: z.string(),
|
||||||
|
}),
|
||||||
|
"onboard-meeting-setup": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
project: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"user-profile": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeepLinkParams = {
|
export type DeepLinkParams = {
|
||||||
|
|||||||
@@ -1074,7 +1074,8 @@ export async function generateEndorserJwtUrlForAccount(
|
|||||||
|
|
||||||
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
||||||
|
|
||||||
const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
const viewPrefix =
|
||||||
|
APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
||||||
return viewPrefix + vcJwt;
|
return viewPrefix + vcJwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faArrowRotateBackward,
|
faArrowRotateBackward,
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
faBitcoinSign,
|
||||||
faBurst,
|
faBurst,
|
||||||
@@ -92,8 +92,8 @@ library.add(
|
|||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faArrowRight,
|
faArrowRight,
|
||||||
faArrowRotateBackward,
|
faArrowRotateBackward,
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faArrowUp,
|
faArrowUp,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
faBan,
|
faBan,
|
||||||
faBitcoinSign,
|
faBitcoinSign,
|
||||||
faBurst,
|
faBurst,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
updateDefaultSettings,
|
updateDefaultSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||||
import {
|
import {
|
||||||
@@ -974,19 +974,16 @@ export interface DatabaseExport {
|
|||||||
*/
|
*/
|
||||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||||
// Convert each contact to a plain object and ensure all fields are included
|
// Convert each contact to a plain object and ensure all fields are included
|
||||||
const rows = contacts.map((contact) => ({
|
const rows = contacts.map((contact) => {
|
||||||
did: contact.did,
|
const exContact: ContactWithJsonStrings = R.omit(
|
||||||
name: contact.name || null,
|
["contactMethods"],
|
||||||
contactMethods: contact.contactMethods
|
contact,
|
||||||
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
);
|
||||||
: null,
|
exContact.contactMethods = contact.contactMethods
|
||||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
? JSON.stringify(contact.contactMethods, [])
|
||||||
notes: contact.notes || null,
|
: undefined;
|
||||||
profileImageUrl: contact.profileImageUrl || null,
|
return exContact;
|
||||||
publicKeyBase64: contact.publicKeyBase64 || null,
|
});
|
||||||
seesMe: contact.seesMe || false,
|
|
||||||
registered: contact.registered || false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ import router from "./router";
|
|||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from "./services/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from "./services/deepLinks";
|
||||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
import { logger, safeStringify } from "./utils/logger";
|
||||||
import { logger } from "./utils/logger";
|
|
||||||
|
|
||||||
logger.log("[Capacitor] Starting initialization");
|
logger.log("[Capacitor] Starting initialization");
|
||||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||||
@@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => {
|
|||||||
await router.isReady();
|
await router.isReady();
|
||||||
await deepLinkHandler.handleDeepLink(data.url);
|
await deepLinkHandler.handleDeepLink(data.url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
logger.error("[DeepLink] Error handling deep link: ", error);
|
||||||
handleApiError(
|
handleApiError(
|
||||||
{
|
{
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : safeStringify(error),
|
||||||
} as AxiosError,
|
} as AxiosError,
|
||||||
"deep-link",
|
"deep-link",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,15 +10,11 @@ import { FontAwesomeIcon } from "./libs/fontawesome";
|
|||||||
import Camera from "simple-vue-camera";
|
import Camera from "simple-vue-camera";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
const platform = process.env.VITE_PLATFORM;
|
// const platform = process.env.VITE_PLATFORM;
|
||||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
// const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
logger.log("Platform", JSON.stringify({ platform }));
|
|
||||||
logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
|
|
||||||
|
|
||||||
// Global Error Handler
|
// Global Error Handler
|
||||||
function setupGlobalErrorHandler(app: VueApp) {
|
function setupGlobalErrorHandler(app: VueApp) {
|
||||||
logger.log("[App Init] Setting up global error handler");
|
|
||||||
app.config.errorHandler = (
|
app.config.errorHandler = (
|
||||||
err: unknown,
|
err: unknown,
|
||||||
instance: ComponentPublicInstance | null,
|
instance: ComponentPublicInstance | null,
|
||||||
@@ -38,30 +34,13 @@ function setupGlobalErrorHandler(app: VueApp) {
|
|||||||
|
|
||||||
// Function to initialize the app
|
// Function to initialize the app
|
||||||
export function initializeApp() {
|
export function initializeApp() {
|
||||||
logger.log("[App Init] Starting app initialization");
|
|
||||||
logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
|
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
logger.log("[App Init] Vue app created");
|
|
||||||
|
|
||||||
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
|
||||||
logger.log("[App Init] Components registered");
|
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
logger.log("[App Init] Pinia store initialized");
|
|
||||||
|
|
||||||
app.use(VueAxios, axios);
|
app.use(VueAxios, axios);
|
||||||
logger.log("[App Init] Axios initialized");
|
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
logger.log("[App Init] Router initialized");
|
|
||||||
|
|
||||||
app.use(Notifications);
|
app.use(Notifications);
|
||||||
logger.log("[App Init] Notifications initialized");
|
|
||||||
|
|
||||||
setupGlobalErrorHandler(app);
|
setupGlobalErrorHandler(app);
|
||||||
logger.log("[App Init] App initialization complete");
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import { logger } from "./utils/logger";
|
|||||||
const platform = process.env.VITE_PLATFORM;
|
const platform = process.env.VITE_PLATFORM;
|
||||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
logger.info("[Web] PWA enabled", { pwa_enabled });
|
|
||||||
logger.info("[Web] Platform", { platform });
|
|
||||||
|
|
||||||
// Only import service worker for web builds
|
// Only import service worker for web builds
|
||||||
if (platform !== "electron" && pwa_enabled) {
|
if (platform !== "electron" && pwa_enabled) {
|
||||||
import("./registerServiceWorker"); // Web PWA support
|
import("./registerServiceWorker"); // Web PWA support
|
||||||
@@ -31,7 +28,7 @@ function sqlInit() {
|
|||||||
if (platform === "web" || platform === "development") {
|
if (platform === "web" || platform === "development") {
|
||||||
sqlInit();
|
sqlInit();
|
||||||
} else {
|
} else {
|
||||||
logger.info("[Web] SQL not initialized for platform", { platform });
|
logger.warn("[Web] SQL not initialized for platform", { platform });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "discover",
|
name: "discover",
|
||||||
component: () => import("../views/DiscoverView.vue"),
|
component: () => import("../views/DiscoverView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/deep-link/:path*",
|
||||||
|
name: "deep-link",
|
||||||
|
component: () => import("../views/DeepLinkRedirectView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/gifted-details",
|
path: "/gifted-details",
|
||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { logger } from "../utils/logger";
|
import { logger, safeStringify } from "../utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles API errors with platform-specific logging and error processing.
|
* Handles API errors with platform-specific logging and error processing.
|
||||||
@@ -37,7 +37,8 @@ import { logger } from "../utils/logger";
|
|||||||
*/
|
*/
|
||||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
|
||||||
|
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
data: error.response?.data,
|
data: error.response?.data,
|
||||||
|
|||||||
@@ -27,18 +27,16 @@
|
|||||||
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
||||||
*
|
*
|
||||||
* Supported Routes:
|
* Supported Routes:
|
||||||
* - user-profile: View user profile
|
|
||||||
* - project: View project details
|
|
||||||
* - onboard-meeting-setup: Setup onboarding meeting
|
|
||||||
* - invite-one-accept: Accept invitation
|
|
||||||
* - contact-import: Import contacts
|
|
||||||
* - confirm-gift: Confirm gift
|
|
||||||
* - claim: View claim
|
* - claim: View claim
|
||||||
* - claim-cert: View claim certificate
|
|
||||||
* - claim-add-raw: Add raw claim
|
* - claim-add-raw: Add raw claim
|
||||||
* - contact-edit: Edit contact
|
* - claim-cert: View claim certificate
|
||||||
* - contacts: View contacts
|
* - confirm-gift
|
||||||
|
* - contact-import: Import contacts
|
||||||
* - did: View DID
|
* - did: View DID
|
||||||
|
* - invite-one-accept: Accept invitation
|
||||||
|
* - onboard-meeting-members
|
||||||
|
* - project: View project details
|
||||||
|
* - user-profile: View user profile
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const handler = new DeepLinkHandler(router);
|
* const handler = new DeepLinkHandler(router);
|
||||||
@@ -81,14 +79,15 @@ export class DeepLinkHandler {
|
|||||||
string,
|
string,
|
||||||
{ name: string; paramKey?: string }
|
{ name: string; paramKey?: string }
|
||||||
> = {
|
> = {
|
||||||
|
// note that similar lists are in src/interfaces/deepLinks.ts
|
||||||
claim: { name: "claim" },
|
claim: { name: "claim" },
|
||||||
"claim-add-raw": { name: "claim-add-raw" },
|
"claim-add-raw": { name: "claim-add-raw" },
|
||||||
"claim-cert": { name: "claim-cert" },
|
"claim-cert": { name: "claim-cert" },
|
||||||
"confirm-gift": { name: "confirm-gift" },
|
"confirm-gift": { name: "confirm-gift" },
|
||||||
|
"contact-import": { name: "contact-import", paramKey: "jwt" },
|
||||||
did: { name: "did", paramKey: "did" },
|
did: { name: "did", paramKey: "did" },
|
||||||
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
||||||
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
||||||
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
|
||||||
project: { name: "project" },
|
project: { name: "project" },
|
||||||
"user-profile": { name: "user-profile" },
|
"user-profile": { name: "user-profile" },
|
||||||
};
|
};
|
||||||
@@ -99,7 +98,7 @@ export class DeepLinkHandler {
|
|||||||
*
|
*
|
||||||
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
||||||
* @throws {DeepLinkError} If URL format is invalid
|
* @throws {DeepLinkError} If URL format is invalid
|
||||||
* @returns Parsed URL components (path, params, query)
|
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
|
||||||
*/
|
*/
|
||||||
private parseDeepLink(url: string) {
|
private parseDeepLink(url: string) {
|
||||||
const parts = url.split("://");
|
const parts = url.split("://");
|
||||||
@@ -115,7 +114,16 @@ export class DeepLinkHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [path, queryString] = parts[1].split("?");
|
const [path, queryString] = parts[1].split("?");
|
||||||
const [routePath, param] = path.split("/");
|
const [routePath, ...pathParams] = path.split("/");
|
||||||
|
// logger.info(
|
||||||
|
// "[DeepLink] Debug:",
|
||||||
|
// "Route Path:",
|
||||||
|
// routePath,
|
||||||
|
// "Path Params:",
|
||||||
|
// pathParams,
|
||||||
|
// "Query String:",
|
||||||
|
// queryString,
|
||||||
|
// );
|
||||||
|
|
||||||
// Validate route exists before proceeding
|
// Validate route exists before proceeding
|
||||||
if (!this.ROUTE_MAP[routePath]) {
|
if (!this.ROUTE_MAP[routePath]) {
|
||||||
@@ -134,45 +142,14 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (param) {
|
if (pathParams) {
|
||||||
// Now we know routePath exists in ROUTE_MAP
|
// Now we know routePath exists in ROUTE_MAP
|
||||||
const routeConfig = this.ROUTE_MAP[routePath];
|
const routeConfig = this.ROUTE_MAP[routePath];
|
||||||
params[routeConfig.paramKey ?? "id"] = param;
|
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
||||||
}
|
}
|
||||||
return { path: routePath, params, query };
|
return { path: routePath, params, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes incoming deep links and routes them appropriately.
|
|
||||||
* Handles validation, error handling, and routing to the correct view.
|
|
||||||
*
|
|
||||||
* @param url - The deep link URL to process
|
|
||||||
* @throws {DeepLinkError} If URL processing fails
|
|
||||||
*/
|
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
|
||||||
const { path, params, query } = this.parseDeepLink(url);
|
|
||||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
|
||||||
const sanitizedParams = Object.fromEntries(
|
|
||||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
|
||||||
);
|
|
||||||
await this.validateAndRoute(path, sanitizedParams, query);
|
|
||||||
} catch (error) {
|
|
||||||
const deepLinkError = error as DeepLinkError;
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
throw {
|
|
||||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
|
||||||
message: deepLinkError.message,
|
|
||||||
details: deepLinkError.details,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes the deep link to appropriate view with validated parameters.
|
* Routes the deep link to appropriate view with validated parameters.
|
||||||
* Validates route and parameters using Zod schemas before routing.
|
* Validates route and parameters using Zod schemas before routing.
|
||||||
@@ -243,6 +220,39 @@ export class DeepLinkHandler {
|
|||||||
code: "INVALID_PARAMETERS",
|
code: "INVALID_PARAMETERS",
|
||||||
message: (error as Error).message,
|
message: (error as Error).message,
|
||||||
details: error,
|
details: error,
|
||||||
|
params: params,
|
||||||
|
query: query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes incoming deep links and routes them appropriately.
|
||||||
|
* Handles validation, error handling, and routing to the correct view.
|
||||||
|
*
|
||||||
|
* @param url - The deep link URL to process
|
||||||
|
* @throws {DeepLinkError} If URL processing fails
|
||||||
|
*/
|
||||||
|
async handleDeepLink(url: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
||||||
|
const { path, params, query } = this.parseDeepLink(url);
|
||||||
|
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||||
|
const sanitizedParams = Object.fromEntries(
|
||||||
|
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||||
|
);
|
||||||
|
await this.validateAndRoute(path, sanitizedParams, query);
|
||||||
|
} catch (error) {
|
||||||
|
const deepLinkError = error as DeepLinkError;
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||||
|
message: deepLinkError.message,
|
||||||
|
details: deepLinkError.details,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import {
|
|||||||
generateUpdateStatement,
|
generateUpdateStatement,
|
||||||
generateInsertStatement,
|
generateInsertStatement,
|
||||||
} from "../db/databaseUtil";
|
} from "../db/databaseUtil";
|
||||||
import { updateDefaultSettings } from "../db/databaseUtil";
|
|
||||||
import { importFromMnemonic } from "../libs/util";
|
import { importFromMnemonic } from "../libs/util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,11 +155,14 @@ export async function getDexieContacts(): Promise<Contact[]> {
|
|||||||
await db.open();
|
await db.open();
|
||||||
const contacts = await db.contacts.toArray();
|
const contacts = await db.contacts.toArray();
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Retrieved ${contacts.length} contacts from Dexie`,
|
`[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from Dexie`,
|
||||||
);
|
);
|
||||||
return contacts;
|
return contacts;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Error retrieving Dexie contacts:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Error retrieving Dexie contacts:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw new Error(`Failed to retrieve Dexie contacts: ${error}`);
|
throw new Error(`Failed to retrieve Dexie contacts: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,11 +216,14 @@ export async function getSqliteContacts(): Promise<Contact[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Retrieved ${contacts.length} contacts from SQLite`,
|
`[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from SQLite`,
|
||||||
);
|
);
|
||||||
return contacts;
|
return contacts;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Error retrieving SQLite contacts:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Error retrieving SQLite contacts:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw new Error(`Failed to retrieve SQLite contacts: ${error}`);
|
throw new Error(`Failed to retrieve SQLite contacts: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,11 +256,14 @@ export async function getDexieSettings(): Promise<Settings[]> {
|
|||||||
await db.open();
|
await db.open();
|
||||||
const settings = await db.settings.toArray();
|
const settings = await db.settings.toArray();
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Retrieved ${settings.length} settings from Dexie`,
|
`[IndexedDBMigrationService] Retrieved ${settings.length} settings from Dexie`,
|
||||||
);
|
);
|
||||||
return settings;
|
return settings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Error retrieving Dexie settings:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Error retrieving Dexie settings:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw new Error(`Failed to retrieve Dexie settings: ${error}`);
|
throw new Error(`Failed to retrieve Dexie settings: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,11 +317,14 @@ export async function getSqliteSettings(): Promise<Settings[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Retrieved ${settings.length} settings from SQLite`,
|
`[IndexedDBMigrationService] Retrieved ${settings.length} settings from SQLite`,
|
||||||
);
|
);
|
||||||
return settings;
|
return settings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Error retrieving SQLite settings:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Error retrieving SQLite settings:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw new Error(`Failed to retrieve SQLite settings: ${error}`);
|
throw new Error(`Failed to retrieve SQLite settings: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,11 +364,14 @@ export async function getSqliteAccounts(): Promise<string[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Retrieved ${dids.length} accounts from SQLite`,
|
`[IndexedDBMigrationService] Retrieved ${dids.length} accounts from SQLite`,
|
||||||
);
|
);
|
||||||
return dids;
|
return dids;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Error retrieving SQLite accounts:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Error retrieving SQLite accounts:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw new Error(`Failed to retrieve SQLite accounts: ${error}`);
|
throw new Error(`Failed to retrieve SQLite accounts: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -391,11 +405,14 @@ export async function getDexieAccounts(): Promise<Account[]> {
|
|||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
const accounts = await accountsDB.accounts.toArray();
|
const accounts = await accountsDB.accounts.toArray();
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Retrieved ${accounts.length} accounts from Dexie`,
|
`[IndexedDBMigrationService] Retrieved ${accounts.length} accounts from Dexie`,
|
||||||
);
|
);
|
||||||
return accounts;
|
return accounts;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Error retrieving Dexie accounts:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Error retrieving Dexie accounts:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
throw new Error(`Failed to retrieve Dexie accounts: ${error}`);
|
throw new Error(`Failed to retrieve Dexie accounts: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -429,7 +446,7 @@ export async function getDexieAccounts(): Promise<Account[]> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function compareDatabases(): Promise<DataComparison> {
|
export async function compareDatabases(): Promise<DataComparison> {
|
||||||
logger.info("[MigrationService] Starting database comparison");
|
logger.info("[IndexedDBMigrationService] Starting database comparison");
|
||||||
|
|
||||||
const [
|
const [
|
||||||
dexieContacts,
|
dexieContacts,
|
||||||
@@ -470,7 +487,7 @@ export async function compareDatabases(): Promise<DataComparison> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info("[MigrationService] Database comparison completed", {
|
logger.info("[IndexedDBMigrationService] Database comparison completed", {
|
||||||
dexieContacts: dexieContacts.length,
|
dexieContacts: dexieContacts.length,
|
||||||
sqliteContacts: sqliteContacts.length,
|
sqliteContacts: sqliteContacts.length,
|
||||||
dexieSettings: dexieSettings.length,
|
dexieSettings: dexieSettings.length,
|
||||||
@@ -679,6 +696,7 @@ function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
function contactsEqual(contact1: Contact, contact2: Contact): boolean {
|
function contactsEqual(contact1: Contact, contact2: Contact): boolean {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const ifEmpty = (arg: any, def: any) => (arg ? arg : def);
|
const ifEmpty = (arg: any, def: any) => (arg ? arg : def);
|
||||||
const contact1Methods =
|
const contact1Methods =
|
||||||
contact1.contactMethods &&
|
contact1.contactMethods &&
|
||||||
@@ -954,7 +972,7 @@ export function generateComparisonYaml(comparison: DataComparison): string {
|
|||||||
export async function migrateContacts(
|
export async function migrateContacts(
|
||||||
overwriteExisting: boolean = false,
|
overwriteExisting: boolean = false,
|
||||||
): Promise<MigrationResult> {
|
): Promise<MigrationResult> {
|
||||||
logger.info("[MigrationService] Starting contact migration", {
|
logger.info("[IndexedDBMigrationService] Starting contact migration", {
|
||||||
overwriteExisting,
|
overwriteExisting,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -990,7 +1008,7 @@ export async function migrateContacts(
|
|||||||
);
|
);
|
||||||
await platformService.dbExec(sql, params);
|
await platformService.dbExec(sql, params);
|
||||||
result.contactsMigrated++;
|
result.contactsMigrated++;
|
||||||
logger.info(`[MigrationService] Updated contact: ${contact.did}`);
|
logger.info(`[IndexedDBMigrationService] Updated contact: ${contact.did}`);
|
||||||
} else {
|
} else {
|
||||||
result.warnings.push(
|
result.warnings.push(
|
||||||
`Contact ${contact.did} already exists, skipping`,
|
`Contact ${contact.did} already exists, skipping`,
|
||||||
@@ -1004,17 +1022,17 @@ export async function migrateContacts(
|
|||||||
);
|
);
|
||||||
await platformService.dbExec(sql, params);
|
await platformService.dbExec(sql, params);
|
||||||
result.contactsMigrated++;
|
result.contactsMigrated++;
|
||||||
logger.info(`[MigrationService] Added contact: ${contact.did}`);
|
logger.info(`[IndexedDBMigrationService] Added contact: ${contact.did}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`;
|
const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`;
|
||||||
logger.error("[MigrationService]", errorMsg);
|
logger.error("[IndexedDBMigrationService]", errorMsg);
|
||||||
result.errors.push(errorMsg);
|
result.errors.push(errorMsg);
|
||||||
result.success = false;
|
result.success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("[MigrationService] Contact migration completed", {
|
logger.info("[IndexedDBMigrationService] Contact migration completed", {
|
||||||
contactsMigrated: result.contactsMigrated,
|
contactsMigrated: result.contactsMigrated,
|
||||||
errors: result.errors.length,
|
errors: result.errors.length,
|
||||||
warnings: result.warnings.length,
|
warnings: result.warnings.length,
|
||||||
@@ -1023,7 +1041,7 @@ export async function migrateContacts(
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = `Contact migration failed: ${error}`;
|
const errorMsg = `Contact migration failed: ${error}`;
|
||||||
logger.error("[MigrationService]", errorMsg);
|
logger.error("[IndexedDBMigrationService]", errorMsg);
|
||||||
result.errors.push(errorMsg);
|
result.errors.push(errorMsg);
|
||||||
result.success = false;
|
result.success = false;
|
||||||
return result;
|
return result;
|
||||||
@@ -1063,7 +1081,7 @@ export async function migrateContacts(
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function migrateSettings(): Promise<MigrationResult> {
|
export async function migrateSettings(): Promise<MigrationResult> {
|
||||||
logger.info("[MigrationService] Starting settings migration");
|
logger.info("[IndexedDBMigrationService] Starting settings migration");
|
||||||
|
|
||||||
const result: MigrationResult = {
|
const result: MigrationResult = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1076,17 +1094,17 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const dexieSettings = await getDexieSettings();
|
const dexieSettings = await getDexieSettings();
|
||||||
logger.info("[MigrationService] Migrating settings", {
|
logger.info("[IndexedDBMigrationService] Migrating settings", {
|
||||||
dexieSettings: dexieSettings.length,
|
dexieSettings: dexieSettings.length,
|
||||||
});
|
});
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
// Create an array of promises for all settings migrations
|
// Create an array of promises for all settings migrations
|
||||||
const migrationPromises = dexieSettings.map(async (setting) => {
|
const migrationPromises = dexieSettings.map(async (setting) => {
|
||||||
logger.info("[MigrationService] Starting to migrate settings", setting);
|
logger.info(
|
||||||
let sqliteSettingRaw:
|
"[IndexedDBMigrationService] Starting to migrate settings",
|
||||||
| { columns: string[]; values: unknown[][] }
|
setting,
|
||||||
| undefined;
|
);
|
||||||
|
|
||||||
// adjust SQL based on the accountDid key, maybe null
|
// adjust SQL based on the accountDid key, maybe null
|
||||||
let conditional: string;
|
let conditional: string;
|
||||||
@@ -1098,15 +1116,18 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
conditional = "accountDid = ?";
|
conditional = "accountDid = ?";
|
||||||
preparams = [setting.accountDid];
|
preparams = [setting.accountDid];
|
||||||
}
|
}
|
||||||
sqliteSettingRaw = await platformService.dbQuery(
|
const sqliteSettingRaw = await platformService.dbQuery(
|
||||||
"SELECT * FROM settings WHERE " + conditional,
|
"SELECT * FROM settings WHERE " + conditional,
|
||||||
preparams,
|
preparams,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info("[MigrationService] Migrating one set of settings:", {
|
logger.info(
|
||||||
setting,
|
"[IndexedDBMigrationService] Migrating one set of settings:",
|
||||||
sqliteSettingRaw,
|
{
|
||||||
});
|
setting,
|
||||||
|
sqliteSettingRaw,
|
||||||
|
},
|
||||||
|
);
|
||||||
if (sqliteSettingRaw?.values?.length) {
|
if (sqliteSettingRaw?.values?.length) {
|
||||||
// should cover the master settings, where accountDid is null
|
// should cover the master settings, where accountDid is null
|
||||||
delete setting.id; // don't conflict with the id in the sqlite database
|
delete setting.id; // don't conflict with the id in the sqlite database
|
||||||
@@ -1117,7 +1138,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
conditional,
|
conditional,
|
||||||
preparams,
|
preparams,
|
||||||
);
|
);
|
||||||
logger.info("[MigrationService] Updating settings", {
|
logger.info("[IndexedDBMigrationService] Updating settings", {
|
||||||
sql,
|
sql,
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -1127,10 +1148,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
// insert new setting
|
// insert new setting
|
||||||
delete setting.id; // don't conflict with the id in the sqlite database
|
delete setting.id; // don't conflict with the id in the sqlite database
|
||||||
delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
|
delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
|
||||||
const { sql, params } = generateInsertStatement(
|
const { sql, params } = generateInsertStatement(setting, "settings");
|
||||||
setting,
|
|
||||||
"settings",
|
|
||||||
);
|
|
||||||
await platformService.dbExec(sql, params);
|
await platformService.dbExec(sql, params);
|
||||||
result.settingsMigrated++;
|
result.settingsMigrated++;
|
||||||
}
|
}
|
||||||
@@ -1140,7 +1158,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
const updatedSettings = await Promise.all(migrationPromises);
|
const updatedSettings = await Promise.all(migrationPromises);
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[MigrationService] Finished migrating settings",
|
"[IndexedDBMigrationService] Finished migrating settings",
|
||||||
updatedSettings,
|
updatedSettings,
|
||||||
result,
|
result,
|
||||||
);
|
);
|
||||||
@@ -1148,7 +1166,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"[MigrationService] Complete settings migration failed:",
|
"[IndexedDBMigrationService] Complete settings migration failed:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
const errorMessage = `Settings migration failed: ${error}`;
|
const errorMessage = `Settings migration failed: ${error}`;
|
||||||
@@ -1192,7 +1210,7 @@ export async function migrateSettings(): Promise<MigrationResult> {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export async function migrateAccounts(): Promise<MigrationResult> {
|
export async function migrateAccounts(): Promise<MigrationResult> {
|
||||||
logger.info("[MigrationService] Starting account migration");
|
logger.info("[IndexedDBMigrationService] Starting account migration");
|
||||||
|
|
||||||
const result: MigrationResult = {
|
const result: MigrationResult = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -1248,14 +1266,17 @@ export async function migrateAccounts(): Promise<MigrationResult> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("[MigrationService] Successfully migrated account", {
|
logger.info(
|
||||||
did,
|
"[IndexedDBMigrationService] Successfully migrated account",
|
||||||
dateCreated: account.dateCreated,
|
{
|
||||||
});
|
did,
|
||||||
|
dateCreated: account.dateCreated,
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = `Failed to migrate account ${did}: ${error}`;
|
const errorMessage = `Failed to migrate account ${did}: ${error}`;
|
||||||
result.errors.push(errorMessage);
|
result.errors.push(errorMessage);
|
||||||
logger.error("[MigrationService] Account migration failed:", {
|
logger.error("[IndexedDBMigrationService] Account migration failed:", {
|
||||||
error,
|
error,
|
||||||
did,
|
did,
|
||||||
});
|
});
|
||||||
@@ -1272,7 +1293,7 @@ export async function migrateAccounts(): Promise<MigrationResult> {
|
|||||||
result.errors.push(errorMessage);
|
result.errors.push(errorMessage);
|
||||||
result.success = false;
|
result.success = false;
|
||||||
logger.error(
|
logger.error(
|
||||||
"[MigrationService] Complete account migration failed:",
|
"[IndexedDBMigrationService] Complete account migration failed:",
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
@@ -1306,11 +1327,11 @@ export async function migrateAll(): Promise<MigrationResult> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info(
|
||||||
"[MigrationService] Starting complete migration from Dexie to SQLite",
|
"[IndexedDBMigrationService] Starting complete migration from Dexie to SQLite",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 1: Migrate Accounts (foundational)
|
// Step 1: Migrate Accounts (foundational)
|
||||||
logger.info("[MigrationService] Step 1: Migrating accounts...");
|
logger.info("[IndexedDBMigrationService] Step 1: Migrating accounts...");
|
||||||
const accountsResult = await migrateAccounts();
|
const accountsResult = await migrateAccounts();
|
||||||
if (!accountsResult.success) {
|
if (!accountsResult.success) {
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
@@ -1322,7 +1343,7 @@ export async function migrateAll(): Promise<MigrationResult> {
|
|||||||
result.warnings.push(...accountsResult.warnings);
|
result.warnings.push(...accountsResult.warnings);
|
||||||
|
|
||||||
// Step 2: Migrate Settings (depends on accounts)
|
// Step 2: Migrate Settings (depends on accounts)
|
||||||
logger.info("[MigrationService] Step 2: Migrating settings...");
|
logger.info("[IndexedDBMigrationService] Step 2: Migrating settings...");
|
||||||
const settingsResult = await migrateSettings();
|
const settingsResult = await migrateSettings();
|
||||||
if (!settingsResult.success) {
|
if (!settingsResult.success) {
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
@@ -1335,7 +1356,7 @@ export async function migrateAll(): Promise<MigrationResult> {
|
|||||||
|
|
||||||
// Step 4: Migrate Contacts (independent, but after accounts for consistency)
|
// Step 4: Migrate Contacts (independent, but after accounts for consistency)
|
||||||
// ... but which is better done through the contact import view
|
// ... but which is better done through the contact import view
|
||||||
// logger.info("[MigrationService] Step 4: Migrating contacts...");
|
// logger.info("[IndexedDBMigrationService] Step 4: Migrating contacts...");
|
||||||
// const contactsResult = await migrateContacts();
|
// const contactsResult = await migrateContacts();
|
||||||
// if (!contactsResult.success) {
|
// if (!contactsResult.success) {
|
||||||
// result.errors.push(
|
// result.errors.push(
|
||||||
@@ -1354,7 +1375,7 @@ export async function migrateAll(): Promise<MigrationResult> {
|
|||||||
result.contactsMigrated;
|
result.contactsMigrated;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
|
`[IndexedDBMigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
|
||||||
{
|
{
|
||||||
accounts: result.accountsMigrated,
|
accounts: result.accountsMigrated,
|
||||||
settings: result.settingsMigrated,
|
settings: result.settingsMigrated,
|
||||||
@@ -1367,7 +1388,10 @@ export async function migrateAll(): Promise<MigrationResult> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = `Complete migration failed: ${error}`;
|
const errorMessage = `Complete migration failed: ${error}`;
|
||||||
result.errors.push(errorMessage);
|
result.errors.push(errorMessage);
|
||||||
logger.error("[MigrationService] Complete migration failed:", error);
|
logger.error(
|
||||||
|
"[IndexedDBMigrationService] Complete migration failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class MigrationRegistry {
|
|||||||
*/
|
*/
|
||||||
registerMigration(migration: Migration): void {
|
registerMigration(migration: Migration): void {
|
||||||
this.migrations.push(migration);
|
this.migrations.push(migration);
|
||||||
logger.info(`[MigrationService] Registered migration: ${migration.name}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +41,6 @@ class MigrationRegistry {
|
|||||||
*/
|
*/
|
||||||
clearMigrations(): void {
|
clearMigrations(): void {
|
||||||
this.migrations = [];
|
this.migrations = [];
|
||||||
logger.info("[MigrationService] Cleared all registered migrations");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,10 +92,6 @@ export async function runMigrations<T>(
|
|||||||
);
|
);
|
||||||
const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
|
const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[MigrationService] Found ${appliedMigrations.size} applied migrations`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get all registered migrations
|
// Get all registered migrations
|
||||||
const migrations = migrationRegistry.getMigrations();
|
const migrations = migrationRegistry.getMigrations();
|
||||||
|
|
||||||
@@ -106,21 +100,12 @@ export async function runMigrations<T>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`[MigrationService] Running ${migrations.length} registered migrations`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run each migration that hasn't been applied yet
|
// Run each migration that hasn't been applied yet
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
if (appliedMigrations.has(migration.name)) {
|
if (appliedMigrations.has(migration.name)) {
|
||||||
logger.info(
|
|
||||||
`[MigrationService] Skipping already applied migration: ${migration.name}`,
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`[MigrationService] Applying migration: ${migration.name}`);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the migration SQL
|
// Execute the migration SQL
|
||||||
await sqlExec(migration.sql);
|
await sqlExec(migration.sql);
|
||||||
@@ -141,8 +126,6 @@ export async function runMigrations<T>(
|
|||||||
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
throw new Error(`Migration ${migration.name} failed: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("[MigrationService] All migrations completed successfully");
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[MigrationService] Migration process failed:", error);
|
logger.error("[MigrationService] Migration process failed:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { logToDb } from "../db/databaseUtil";
|
import { logToDb } from "../db/databaseUtil";
|
||||||
|
|
||||||
function safeStringify(obj: unknown) {
|
export function safeStringify(obj: unknown) {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
|
|
||||||
return JSON.stringify(obj, (_key, value) => {
|
return JSON.stringify(obj, (_key, value) => {
|
||||||
@@ -52,23 +52,18 @@ export const logger = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
warn: (message: string, ...args: unknown[]) => {
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
// eslint-disable-next-line no-console
|
||||||
process.env.NODE_ENV !== "production" ||
|
console.warn(message, ...args);
|
||||||
process.env.VITE_PLATFORM === "capacitor" ||
|
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
process.env.VITE_PLATFORM === "electron"
|
logToDb(message + argsString);
|
||||||
) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn(message, ...args);
|
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
|
||||||
logToDb(message + argsString);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: (message: string, ...args: unknown[]) => {
|
error: (message: string, ...args: unknown[]) => {
|
||||||
// Errors will always be logged
|
// Errors will always be logged
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message, ...args);
|
console.error(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const messageString = safeStringify(message);
|
||||||
logToDb(message + argsString);
|
const argsString = args.length > 0 ? safeStringify(args) : "";
|
||||||
|
logToDb(messageString + argsString);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -349,8 +349,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
<div v-if="includeUserProfileLocation" class="mb-4 aspect-video">
|
||||||
<p class="text-sm mb-2 text-slate-500">
|
<p class="text-sm mb-2 text-slate-500">
|
||||||
For your security, choose a location nearby but not exactly at your
|
The location you choose will be shared with the world until you remove
|
||||||
place.
|
this checkbox. For your security, choose a location nearby but not
|
||||||
|
exactly at your true location, like at your town center.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<l-map
|
<l-map
|
||||||
@@ -435,11 +436,11 @@
|
|||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
You have done
|
You have done
|
||||||
<b
|
<b
|
||||||
>{{ endorserLimits?.doneClaimsThisWeek || "?" }} claim{{
|
>{{ endorserLimits?.doneClaimsThisWeek ?? "?" }} claim{{
|
||||||
endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
|
endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
|
||||||
}}</b
|
}}</b
|
||||||
>
|
>
|
||||||
out of <b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this
|
out of <b>{{ endorserLimits?.maxClaimsPerWeek ?? "?" }}</b> for this
|
||||||
week. Your claims counter resets at
|
week. Your claims counter resets at
|
||||||
<b class="whitespace-nowrap">{{
|
<b class="whitespace-nowrap">{{
|
||||||
readableDate(endorserLimits?.nextWeekBeginDateTime)
|
readableDate(endorserLimits?.nextWeekBeginDateTime)
|
||||||
@@ -449,14 +450,14 @@
|
|||||||
You have done
|
You have done
|
||||||
<b
|
<b
|
||||||
>{{
|
>{{
|
||||||
endorserLimits?.doneRegistrationsThisMonth || "?"
|
endorserLimits?.doneRegistrationsThisMonth ?? "?"
|
||||||
}}
|
}}
|
||||||
registration{{
|
registration{{
|
||||||
endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
|
endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
|
||||||
}}</b
|
}}</b
|
||||||
>
|
>
|
||||||
out of
|
out of
|
||||||
<b>{{ endorserLimits?.maxRegistrationsPerMonth || "?" }}</b> for this
|
<b>{{ endorserLimits?.maxRegistrationsPerMonth ?? "?" }}</b> for this
|
||||||
this month.
|
this month.
|
||||||
<i>(You cannot register anyone on your first day.)</i>
|
<i>(You cannot register anyone on your first day.)</i>
|
||||||
Your registration counter resets at
|
Your registration counter resets at
|
||||||
@@ -467,11 +468,11 @@
|
|||||||
<p class="mt-3 text-sm">
|
<p class="mt-3 text-sm">
|
||||||
You have uploaded
|
You have uploaded
|
||||||
<b
|
<b
|
||||||
>{{ imageLimits?.doneImagesThisWeek || "?" }} image{{
|
>{{ imageLimits?.doneImagesThisWeek ?? "?" }} image{{
|
||||||
imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
|
imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
|
||||||
}}</b
|
}}</b
|
||||||
>
|
>
|
||||||
out of <b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this
|
out of <b>{{ imageLimits?.maxImagesPerWeek ?? "?" }}</b> for this
|
||||||
week. Your image counter resets at
|
week. Your image counter resets at
|
||||||
<b class="whitespace-nowrap">{{
|
<b class="whitespace-nowrap">{{
|
||||||
readableDate(imageLimits?.nextWeekBeginDateTime)
|
readableDate(imageLimits?.nextWeekBeginDateTime)
|
||||||
|
|||||||
@@ -49,21 +49,32 @@
|
|||||||
v-if="veriClaim.id"
|
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="View Printable Certificate"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="square"
|
icon="square"
|
||||||
class="text-white bg-yellow-500 p-1"
|
class="text-white bg-yellow-500 p-1"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<button
|
||||||
|
v-if="veriClaim.id"
|
||||||
|
class="text-blue-500 ml-2 mt-2"
|
||||||
|
title="Copy Printable Certificate Link"
|
||||||
|
@click="
|
||||||
|
copyToClipboard(
|
||||||
|
'A link to the certificate page',
|
||||||
|
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<font-awesome icon="link" class="text-yellow-500 p-1" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- show link icon to copy this URL to the clipboard -->
|
<!-- show link icon to copy this URL to the clipboard -->
|
||||||
<div class="flex justify-end w-full">
|
<div class="flex justify-end w-full">
|
||||||
<button
|
<button
|
||||||
title="Copy Link"
|
title="Copy Link"
|
||||||
@click="
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
copyToClipboard('A link to this page', window.location.href)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<font-awesome icon="link" class="text-slate-500" />
|
<font-awesome icon="link" class="text-slate-500" />
|
||||||
</button>
|
</button>
|
||||||
@@ -405,7 +416,7 @@
|
|||||||
contacts can see more details:
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
>click to copy this page info</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they can make an introduction. Someone is connected to
|
and see if they can make an introduction. Someone is connected to
|
||||||
@@ -428,7 +439,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
>
|
>
|
||||||
@@ -546,7 +557,7 @@ import { useClipboard } from "@vueuse/core";
|
|||||||
import { GenericVerifiableCredential } from "../interfaces";
|
import { GenericVerifiableCredential } from "../interfaces";
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { db } from "../db/index";
|
import { db } from "../db/index";
|
||||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
@@ -593,8 +604,9 @@ export default class ClaimView extends Vue {
|
|||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
veriClaimDump = "";
|
veriClaimDump = "";
|
||||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||||
windowLocation = window.location.href;
|
windowDeepLink = window.location.href; // changed in the setup for deep linking
|
||||||
|
|
||||||
|
APP_SERVER = APP_SERVER;
|
||||||
R = R;
|
R = R;
|
||||||
yaml = yaml;
|
yaml = yaml;
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
@@ -671,6 +683,7 @@ export default class ClaimView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
|
||||||
|
|
||||||
this.canShare = !!navigator.share;
|
this.canShare = !!navigator.share;
|
||||||
}
|
}
|
||||||
@@ -1006,11 +1019,11 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard("A link to this page", this.windowDeepLink);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.windowDeepLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
@@ -494,7 +494,7 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
veriClaimDump = "";
|
veriClaimDump = "";
|
||||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||||
windowLocation = window.location.href;
|
windowLocation = window.location.href; // this is changed to a deep link in the setup
|
||||||
|
|
||||||
R = R;
|
R = R;
|
||||||
yaml = yaml;
|
yaml = yaml;
|
||||||
@@ -566,6 +566,9 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const claimId = decodeURIComponent(pathParam);
|
const claimId = decodeURIComponent(pathParam);
|
||||||
|
|
||||||
|
this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId;
|
||||||
|
|
||||||
await this.loadClaim(claimId, this.activeDid);
|
await this.loadClaim(claimId, this.activeDid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,12 +679,12 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Add participant (giver/recipient) name & URL info
|
* Add participant (giver/recipient) name & URL info
|
||||||
*/
|
*/
|
||||||
|
this.giverName = this.didInfo(this.giveDetails?.agentDid);
|
||||||
if (this.giveDetails?.agentDid) {
|
if (this.giveDetails?.agentDid) {
|
||||||
this.giverName = this.didInfo(this.giveDetails.agentDid);
|
|
||||||
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
|
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
|
||||||
}
|
}
|
||||||
|
this.recipientName = this.didInfo(this.giveDetails?.recipientDid);
|
||||||
if (this.giveDetails?.recipientDid) {
|
if (this.giveDetails?.recipientDid) {
|
||||||
this.recipientName = this.didInfo(this.giveDetails.recipientDid);
|
|
||||||
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
|
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,12 +124,14 @@ import * as databaseUtil from "../db/databaseUtil";
|
|||||||
import {
|
import {
|
||||||
CONTACT_CSV_HEADER,
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
|
generateEndorserJwtUrlForAccount,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
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";
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -157,6 +159,7 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
givenName = "";
|
givenName = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
|
profileImageUrl = "";
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||||
|
|
||||||
@@ -179,6 +182,7 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
this.profileImageUrl = settings.profileImageUrl || "";
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
const account = await retrieveAccountMetadata(this.activeDid);
|
||||||
if (account) {
|
if (account) {
|
||||||
@@ -588,9 +592,19 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyUrlToClipboard() {
|
async onCopyUrlToClipboard() {
|
||||||
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||||
|
this.activeDid,
|
||||||
|
)) as Account;
|
||||||
|
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||||
|
account,
|
||||||
|
this.isRegistered,
|
||||||
|
this.givenName,
|
||||||
|
this.profileImageUrl,
|
||||||
|
true,
|
||||||
|
);
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(this.qrValue)
|
.copy(jwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
|||||||
import {
|
import {
|
||||||
CONTACT_CSV_HEADER,
|
CONTACT_CSV_HEADER,
|
||||||
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
|
generateEndorserJwtUrlForAccount,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
@@ -187,6 +188,7 @@ import { logger } from "../utils/logger";
|
|||||||
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||||
import { CameraState } from "@/services/QRScanner/types";
|
import { CameraState } from "@/services/QRScanner/types";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -216,6 +218,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
isScanning = false;
|
isScanning = false;
|
||||||
|
profileImageUrl = "";
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
|
|
||||||
// QR Scanner properties
|
// QR Scanner properties
|
||||||
@@ -253,6 +256,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.hideRegisterPromptOnNewContact =
|
this.hideRegisterPromptOnNewContact =
|
||||||
!!settings.hideRegisterPromptOnNewContact;
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
this.profileImageUrl = settings.profileImageUrl || "";
|
||||||
|
|
||||||
const account = await libsUtil.retrieveAccountMetadata(this.activeDid);
|
const account = await libsUtil.retrieveAccountMetadata(this.activeDid);
|
||||||
if (account) {
|
if (account) {
|
||||||
@@ -667,10 +671,19 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyUrlToClipboard() {
|
async onCopyUrlToClipboard() {
|
||||||
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||||
|
this.activeDid,
|
||||||
|
)) as Account;
|
||||||
|
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||||
|
account,
|
||||||
|
this.isRegistered,
|
||||||
|
this.givenName,
|
||||||
|
this.profileImageUrl,
|
||||||
|
true,
|
||||||
|
);
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(this.qrValue)
|
.copy(jwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -126,7 +126,6 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
v-if="showGiveNumbers"
|
v-if="showGiveNumbers"
|
||||||
href=""
|
|
||||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||||
:class="showGiveAmountsClassNames()"
|
:class="showGiveAmountsClassNames()"
|
||||||
@click="toggleShowGiveTotals()"
|
@click="toggleShowGiveTotals()"
|
||||||
@@ -142,7 +141,6 @@
|
|||||||
</button>
|
</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"
|
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()"
|
@click="toggleShowContactAmounts()"
|
||||||
>
|
>
|
||||||
@@ -493,7 +491,7 @@ export default class ContactsView extends Vue {
|
|||||||
private async processContactJwt() {
|
private async processContactJwt() {
|
||||||
// handle a contact sent via URL
|
// handle a contact sent via URL
|
||||||
//
|
//
|
||||||
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
|
// For external links, use /deep-link/contact-import/:jwt with a JWT that has an array of contacts
|
||||||
// because that will do better error checking for things like missing data on iOS platforms.
|
// because that will do better error checking for things like missing data on iOS platforms.
|
||||||
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
||||||
if (importedContactJwt) {
|
if (importedContactJwt) {
|
||||||
@@ -619,7 +617,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Error with Invite",
|
title: "Error with Invite",
|
||||||
text: message,
|
text: message,
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||||
@@ -1124,7 +1122,7 @@ export default class ContactsView extends Vue {
|
|||||||
(regResult.error as string) ||
|
(regResult.error as string) ||
|
||||||
"Something went wrong during registration.",
|
"Something went wrong during registration.",
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1158,7 +1156,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Registration Error",
|
title: "Registration Error",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1397,7 +1395,8 @@ export default class ContactsView extends Vue {
|
|||||||
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||||
contacts: selectedContacts,
|
contacts: selectedContacts,
|
||||||
});
|
});
|
||||||
const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
|
const contactsJwtUrl =
|
||||||
|
APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(contactsJwtUrl)
|
.copy(contactsJwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -77,6 +77,7 @@
|
|||||||
@click="confirmSetVisibility(contactFromDid, false)"
|
@click="confirmSetVisibility(contactFromDid, false)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="eye" class="fa-fw" />
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -87,6 +88,32 @@
|
|||||||
@click="confirmSetVisibility(contactFromDid, true)"
|
@click="confirmSetVisibility(contactFromDid, true)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-if="
|
||||||
|
contactFromDid?.iViewContent &&
|
||||||
|
contactFromDid.did !== activeDid
|
||||||
|
"
|
||||||
|
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
|
title="I view their content"
|
||||||
|
@click="confirmViewContent(contactFromDid, false)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="
|
||||||
|
!contactFromDid?.iViewContent &&
|
||||||
|
contactFromDid?.did !== activeDid
|
||||||
|
"
|
||||||
|
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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
|
title="I do not view their content"
|
||||||
|
@click="confirmViewContent(contactFromDid, true)"
|
||||||
|
>
|
||||||
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -825,9 +852,9 @@ export default class DIDView extends Vue {
|
|||||||
title: "Visibility Refreshed",
|
title: "Visibility Refreshed",
|
||||||
text:
|
text:
|
||||||
libsUtil.nameForContact(contact, true) +
|
libsUtil.nameForContact(contact, true) +
|
||||||
" can " +
|
" can" +
|
||||||
(visibility ? "" : "not ") +
|
(visibility ? "" : " not") +
|
||||||
"see your activity.",
|
" see your activity.",
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
);
|
);
|
||||||
@@ -857,6 +884,64 @@ export default class DIDView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm whether the user want to see/hide the other's content, then execute it
|
||||||
|
*
|
||||||
|
* @param contact Contact content to show/hide from user
|
||||||
|
* @param view whether user wants to view this contact
|
||||||
|
*/
|
||||||
|
async confirmViewContent(contact: Contact, view: boolean) {
|
||||||
|
const contentVisibilityPrompt = view
|
||||||
|
? "Are you sure you want to see their content?"
|
||||||
|
: "Are you sure you want to hide their content from you?";
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "modal",
|
||||||
|
type: "confirm",
|
||||||
|
title: "Set Content Visibility",
|
||||||
|
text: contentVisibilityPrompt,
|
||||||
|
onYes: async () => {
|
||||||
|
const success = await this.setViewContent(contact, view);
|
||||||
|
if (success) {
|
||||||
|
contact.iViewContent = view; // see visibility note about not working inside setVisibility
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates contact content visibility for this device
|
||||||
|
*
|
||||||
|
* @param contact - Contact to update content visibility for
|
||||||
|
* @param visibility - New content visibility state
|
||||||
|
* @returns Boolean indicating success
|
||||||
|
*/
|
||||||
|
async setViewContent(contact: Contact, visibility: boolean) {
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
await platformService.dbExec(
|
||||||
|
"UPDATE contacts SET iViewContent = ? WHERE did = ?",
|
||||||
|
[visibility, contact.did],
|
||||||
|
);
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
db.contacts.update(contact.did, { iViewContent: visibility });
|
||||||
|
}
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "Visibility Set",
|
||||||
|
text:
|
||||||
|
"You will" +
|
||||||
|
(visibility ? "" : " not") +
|
||||||
|
` see ${contact.name}'s activity.`,
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
icon-name="chart"
|
icon-name="chart"
|
||||||
svg-class="-ml-1 mr-3 h-5 w-5"
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
||||||
/>
|
/>
|
||||||
Download Account
|
Show Account Seed
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -1122,6 +1122,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
private loadingMessage = "";
|
private loadingMessage = "";
|
||||||
private error = "";
|
private error = "";
|
||||||
private warning = "";
|
private warning = "";
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private exportedData: Record<string, any> | null = null;
|
private exportedData: Record<string, any> | null = null;
|
||||||
private successMessage = "";
|
private successMessage = "";
|
||||||
|
|
||||||
@@ -1134,6 +1135,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
* @param {any} setting - The setting object
|
* @param {any} setting - The setting object
|
||||||
* @returns {string} The display name for the setting
|
* @returns {string} The display name for the setting
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getSettingDisplayName(setting: any): string {
|
getSettingDisplayName(setting: any): string {
|
||||||
// Handle exported JSON format (has 'type' and 'did' fields)
|
// Handle exported JSON format (has 'type' and 'did' fields)
|
||||||
if (setting.type && setting.did) {
|
if (setting.type && setting.did) {
|
||||||
@@ -1153,6 +1155,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
* @param {any} account - The account object
|
* @param {any} account - The account object
|
||||||
* @returns {boolean} True if account has identity
|
* @returns {boolean} True if account has identity
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getAccountHasIdentity(account: any): boolean {
|
getAccountHasIdentity(account: any): boolean {
|
||||||
// Handle exported JSON format (has 'hasIdentity' field)
|
// Handle exported JSON format (has 'hasIdentity' field)
|
||||||
if (account.hasIdentity !== undefined) {
|
if (account.hasIdentity !== undefined) {
|
||||||
@@ -1170,6 +1173,7 @@ export default class DatabaseMigration extends Vue {
|
|||||||
* @param {any} account - The account object
|
* @param {any} account - The account object
|
||||||
* @returns {boolean} True if account has mnemonic
|
* @returns {boolean} True if account has mnemonic
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getAccountHasMnemonic(account: any): boolean {
|
getAccountHasMnemonic(account: any): boolean {
|
||||||
// Handle exported JSON format (has 'hasMnemonic' field)
|
// Handle exported JSON format (has 'hasMnemonic' field)
|
||||||
if (account.hasMnemonic !== undefined) {
|
if (account.hasMnemonic !== undefined) {
|
||||||
|
|||||||
@@ -66,9 +66,14 @@ const formattedPath = computed(() => {
|
|||||||
const path = originalPath.value.replace(/^\/+/, "");
|
const path = originalPath.value.replace(/^\/+/, "");
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
logger.log("Original Path:", originalPath.value);
|
logger.log(
|
||||||
logger.log("Route Params:", route.params);
|
"[DeepLinkError] Original Path:",
|
||||||
logger.log("Route Query:", route.query);
|
originalPath.value,
|
||||||
|
"Route Params:",
|
||||||
|
route.params,
|
||||||
|
"Route Query:",
|
||||||
|
route.query,
|
||||||
|
);
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
});
|
});
|
||||||
|
|||||||
227
src/views/DeepLinkRedirectView.vue
Normal file
227
src/views/DeepLinkRedirectView.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
||||||
|
<div
|
||||||
|
class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
|
||||||
|
>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||||
|
Redirecting to Time Safari
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div v-if="destinationUrl" class="space-y-4">
|
||||||
|
<!-- Platform-specific messaging -->
|
||||||
|
<div class="text-center text-gray-600 mb-4">
|
||||||
|
<p v-if="isMobile">
|
||||||
|
{{
|
||||||
|
isIOS
|
||||||
|
? "Opening Time Safari app on your iPhone..."
|
||||||
|
: "Opening Time Safari app on your Android device..."
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p v-else>Opening Time Safari app...</p>
|
||||||
|
<p class="text-sm mt-2">
|
||||||
|
<span v-if="isMobile"
|
||||||
|
>If the app doesn't open automatically, use one of these
|
||||||
|
options:</span
|
||||||
|
>
|
||||||
|
<span v-else>Choose how you'd like to open this link:</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deep Link Button -->
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
:href="deepLinkUrl || '#'"
|
||||||
|
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
@click="handleDeepLinkClick"
|
||||||
|
>
|
||||||
|
<span v-if="isMobile">Open in Time Safari App</span>
|
||||||
|
<span v-else>Try Opening in Time Safari App</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Web Fallback Link -->
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
:href="webUrl || '#'"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
||||||
|
@click="handleWebFallbackClick"
|
||||||
|
>
|
||||||
|
<span v-if="isMobile">Open in Web Browser Instead</span>
|
||||||
|
<span v-else>Open in Web Browser</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Instructions -->
|
||||||
|
<div class="text-center text-sm text-gray-500 mt-4">
|
||||||
|
<p v-if="isMobile">
|
||||||
|
Or manually open:
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||||
|
deepLinkUrl
|
||||||
|
}}</code>
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
If you have the Time Safari app installed, you can also copy this
|
||||||
|
link:
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||||
|
deepLinkUrl
|
||||||
|
}}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Platform info for debugging -->
|
||||||
|
<div
|
||||||
|
v-if="isDevelopment"
|
||||||
|
class="text-center text-xs text-gray-400 mt-4"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||||
|
</p>
|
||||||
|
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||||
|
{{ pageError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-gray-600">
|
||||||
|
<p>Processing redirect...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
|
import { APP_SERVER } from "@/constants/app";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
import { errorStringForLog } from "@/libs/endorserServer";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class DeepLinkRedirectView extends Vue {
|
||||||
|
$router!: Router;
|
||||||
|
$route!: RouteLocationNormalizedLoaded;
|
||||||
|
pageError: string | null = null;
|
||||||
|
destinationUrl: string | null = null; // full path after "/deep-link/"
|
||||||
|
deepLinkUrl: string | null = null; // mobile link starting "timesafari://"
|
||||||
|
webUrl: string | null = null; // web link, eg "https://timesafari.app/..."
|
||||||
|
isDevelopment: boolean = false;
|
||||||
|
userAgent: string = "";
|
||||||
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Get the path from the route parameter (catch-all parameter)
|
||||||
|
const pathParam = this.$route.params.path;
|
||||||
|
|
||||||
|
// If pathParam is an array (catch-all parameter), join it
|
||||||
|
const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam;
|
||||||
|
|
||||||
|
// Get query parameters from the route
|
||||||
|
const queryParams = this.$route.query;
|
||||||
|
|
||||||
|
// Build query string if there are query parameters
|
||||||
|
let queryString = "";
|
||||||
|
if (Object.keys(queryParams).length > 0) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(queryParams).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
const stringValue = Array.isArray(value) ? value[0] : value;
|
||||||
|
if (stringValue !== null && stringValue !== undefined) {
|
||||||
|
searchParams.append(key, stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
queryString = "?" + searchParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine path with query parameters
|
||||||
|
const fullPathWithQuery = fullPath + queryString;
|
||||||
|
|
||||||
|
this.destinationUrl = fullPathWithQuery;
|
||||||
|
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
|
||||||
|
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
|
||||||
|
|
||||||
|
this.isDevelopment = process.env.NODE_ENV !== "production";
|
||||||
|
this.userAgent = navigator.userAgent;
|
||||||
|
|
||||||
|
this.openDeepLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDeepLink() {
|
||||||
|
if (!this.deepLinkUrl || !this.webUrl) {
|
||||||
|
this.pageError =
|
||||||
|
"No deep link was provided. Check the URL and try again.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For mobile, try the deep link URL; for desktop, use the web URL
|
||||||
|
const redirectUrl = this.isMobile ? this.deepLinkUrl : this.webUrl;
|
||||||
|
|
||||||
|
// Method 1: Try window.location.href (works on most browsers)
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
|
||||||
|
// Method 2: Fallback - create and click a link element
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = redirectUrl;
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Fallback deep link failed: " + errorStringForLog(error),
|
||||||
|
);
|
||||||
|
this.pageError =
|
||||||
|
"Redirecting to the Time Safari app failed. Please use a manual option below.";
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Deep link redirect failed: " + errorStringForLog(error));
|
||||||
|
this.pageError =
|
||||||
|
"Unable to open the Time Safari app. Please use a manual option below.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDeepLinkClick(event: Event) {
|
||||||
|
if (!this.deepLinkUrl) return;
|
||||||
|
|
||||||
|
// Prevent default to handle the click manually
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.openDeepLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWebFallbackClick(event: Event) {
|
||||||
|
if (!this.webUrl) return;
|
||||||
|
|
||||||
|
// Get platform capabilities
|
||||||
|
const capabilities = this.platformService.getCapabilities();
|
||||||
|
|
||||||
|
// For mobile, try to open in a new tab/window
|
||||||
|
if (capabilities.isMobile) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.open(this.webUrl, "_blank");
|
||||||
|
}
|
||||||
|
// For desktop, let the default behavior happen (opens in same tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed properties for template
|
||||||
|
get isMobile(): boolean {
|
||||||
|
return this.platformService.getCapabilities().isMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isIOS(): boolean {
|
||||||
|
return this.platformService.getCapabilities().isIOS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -12,6 +12,7 @@ Raymer * @version 1.0.0 */
|
|||||||
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
|
||||||
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
|
<h1 id="ViewHeading" class="text-4xl text-center font-light mb-8">
|
||||||
{{ AppString.APP_NAME }}
|
{{ AppString.APP_NAME }}
|
||||||
|
<span class="text-xs text-gray-500">{{ package.version }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<OnboardingDialog ref="onboardingDialog" />
|
<OnboardingDialog ref="onboardingDialog" />
|
||||||
@@ -106,12 +107,12 @@ Raymer * @version 1.0.0 */
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<UserNameDialog ref="userNameDialog" />
|
<UserNameDialog ref="userNameDialog" />
|
||||||
<div v-if="PASSKEYS_ENABLED" class="flex justify-end w-full">
|
<div class="flex justify-end w-full">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'start' }"
|
:to="{ name: 'start' }"
|
||||||
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||||
>
|
>
|
||||||
See all your options first
|
See advanced options
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,6 +354,7 @@ import * as serverUtil from "../libs/endorserServer";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { GiveRecordWithContactInfo } from "../interfaces/give";
|
import { GiveRecordWithContactInfo } from "../interfaces/give";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import * as Package from "../../package.json";
|
||||||
|
|
||||||
interface Claim {
|
interface Claim {
|
||||||
claim?: Claim; // For nested claims in Verifiable Credentials
|
claim?: Claim; // For nested claims in Verifiable Credentials
|
||||||
@@ -443,11 +445,13 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
AppString = AppString;
|
AppString = AppString;
|
||||||
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
|
PASSKEYS_ENABLED = PASSKEYS_ENABLED;
|
||||||
|
package = Package;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
allContacts: Array<Contact> = [];
|
allContacts: Array<Contact> = [];
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
|
blockedContactDids: Array<string> = [];
|
||||||
feedData: GiveRecordWithContactInfo[] = [];
|
feedData: GiveRecordWithContactInfo[] = [];
|
||||||
feedPreviousOldestId?: string;
|
feedPreviousOldestId?: string;
|
||||||
feedLastViewedClaimId?: string;
|
feedLastViewedClaimId?: string;
|
||||||
@@ -519,7 +523,6 @@ export default class HomeView extends Vue {
|
|||||||
// Retrieve DIDs with better error handling
|
// Retrieve DIDs with better error handling
|
||||||
try {
|
try {
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -552,9 +555,6 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Failed to retrieve settings: ${error}`,
|
`[HomeView] Failed to retrieve settings: ${error}`,
|
||||||
@@ -571,25 +571,14 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
// Load contacts with graceful fallback
|
// Load contacts with graceful fallback
|
||||||
try {
|
try {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
this.loadContacts();
|
||||||
const dbContacts = await platformService.dbQuery(
|
|
||||||
"SELECT * FROM contacts",
|
|
||||||
);
|
|
||||||
this.allContacts = databaseUtil.mapQueryResultToValues(
|
|
||||||
dbContacts,
|
|
||||||
) as Contact[];
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
|
||||||
}
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Failed to retrieve contacts: ${error}`,
|
`[HomeView] Failed to retrieve contacts: ${error}`,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
this.allContacts = []; // Ensure we have a valid empty array
|
this.allContacts = []; // Ensure we have a valid empty array
|
||||||
|
this.blockedContactDids = [];
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -641,9 +630,6 @@ export default class HomeView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.isRegistered = true;
|
this.isRegistered = true;
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] User ${this.activeDid} is now registered`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
@@ -685,11 +671,6 @@ export default class HomeView extends Vue {
|
|||||||
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
||||||
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
||||||
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
||||||
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
|
|
||||||
`${this.numNewOffersToUserProjects} project offers`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
@@ -761,6 +742,9 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
}
|
}
|
||||||
|
this.blockedContactDids = this.allContacts
|
||||||
|
.filter((c) => !c.iViewContent)
|
||||||
|
.map((c) => c.did);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1028,6 +1012,7 @@ export default class HomeView extends Vue {
|
|||||||
);
|
);
|
||||||
if (results.data.length > 0) {
|
if (results.data.length > 0) {
|
||||||
endOfResults = false;
|
endOfResults = false;
|
||||||
|
// gather any contacts that user has blocked from view
|
||||||
await this.processFeedResults(results.data);
|
await this.processFeedResults(results.data);
|
||||||
await this.updateFeedLastViewedId(results.data);
|
await this.updateFeedLastViewedId(results.data);
|
||||||
}
|
}
|
||||||
@@ -1215,7 +1200,7 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if record should be included based on filters
|
* Checks if record should be included based on filters & preferences
|
||||||
*
|
*
|
||||||
* @internal
|
* @internal
|
||||||
* @callGraph
|
* @callGraph
|
||||||
@@ -1241,6 +1226,10 @@ export default class HomeView extends Vue {
|
|||||||
record: GiveSummaryRecord,
|
record: GiveSummaryRecord,
|
||||||
fulfillsPlan?: FulfillsPlan,
|
fulfillsPlan?: FulfillsPlan,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
if (this.blockedContactDids.includes(record.issuerDid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isAnyFeedFilterOn) {
|
if (!this.isAnyFeedFilterOn) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ export default class InviteOneView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inviteLink(jwt: string): string {
|
inviteLink(jwt: string): string {
|
||||||
return APP_SERVER + "/invite-one-accept/" + jwt;
|
return APP_SERVER + "/deep-link/invite-one-accept/" + jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInviteAndNotify(inviteId: string, jwt: string) {
|
copyInviteAndNotify(inviteId: string, jwt: string) {
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
|
|
||||||
onboardMeetingMembersLink(): string {
|
onboardMeetingMembersLink(): string {
|
||||||
if (this.currentMeeting) {
|
if (this.currentMeeting) {
|
||||||
return `${APP_SERVER}/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
return `${APP_SERVER}/deep-link/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
||||||
this.currentMeeting?.password || "",
|
this.currentMeeting?.password || "",
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
>
|
>
|
||||||
<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" />
|
||||||
</button>
|
</button>
|
||||||
|
<button title="Copy Link to Project" @click="onCopyLinkClick()">
|
||||||
|
<font-awesome
|
||||||
|
icon="link"
|
||||||
|
class="text-sm text-slate-500 ml-2 mb-1"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +61,11 @@
|
|||||||
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
|
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
|
||||||
{{ issuerInfoObject?.displayName }}
|
{{ issuerInfoObject?.displayName }}
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-flex items-center">
|
|
||||||
|
<span
|
||||||
|
v-if="!serverUtil.isHiddenDid(issuer)"
|
||||||
|
class="inline-flex items-center"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
path: '/did/' + encodeURIComponent(issuer),
|
path: '/did/' + encodeURIComponent(issuer),
|
||||||
@@ -113,7 +123,7 @@
|
|||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
></font-awesome>
|
></font-awesome>
|
||||||
<a
|
<a
|
||||||
:href="addScheme(url)"
|
:href="ensureScheme(url)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline text-blue-500"
|
class="underline text-blue-500"
|
||||||
>
|
>
|
||||||
@@ -632,7 +642,7 @@ import TopMessage from "../components/TopMessage.vue";
|
|||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
import EntityIcon from "../components/EntityIcon.vue";
|
||||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
@@ -646,6 +656,7 @@ import { retrieveAccountDids } from "../libs/util";
|
|||||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
/**
|
/**
|
||||||
* Project View Component
|
* Project View Component
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
@@ -842,6 +853,28 @@ export default class ProjectViewView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopyLinkClick() {
|
||||||
|
const shortestProjectId = this.projectId.startsWith(
|
||||||
|
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||||
|
)
|
||||||
|
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
|
||||||
|
: this.projectId;
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
||||||
|
useClipboard()
|
||||||
|
.copy(deepLink)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Copied",
|
||||||
|
text: "A link to this project was copied to the clipboard.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
// Isn't there a better way to make this available to the template?
|
||||||
expandText() {
|
expandText() {
|
||||||
this.expanded = true;
|
this.expanded = true;
|
||||||
@@ -1304,7 +1337,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return an HTTPS URL if it's not a global URL
|
// return an HTTPS URL if it's not a global URL
|
||||||
addScheme(url: string) {
|
ensureScheme(url: string) {
|
||||||
if (!libsUtil.isGlobalUri(url)) {
|
if (!libsUtil.isGlobalUri(url)) {
|
||||||
return "https://" + url;
|
return "https://" + url;
|
||||||
}
|
}
|
||||||
@@ -1465,7 +1498,13 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openHiddenDidDialog() {
|
openHiddenDidDialog() {
|
||||||
|
const shortestProjectId = this.projectId.startsWith(
|
||||||
|
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||||
|
)
|
||||||
|
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
|
||||||
|
: this.projectId;
|
||||||
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
||||||
|
"project/" + shortestProjectId,
|
||||||
"creator",
|
"creator",
|
||||||
this.issuerVisibleToDids,
|
this.issuerVisibleToDids,
|
||||||
this.allContacts,
|
this.allContacts,
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
"HUR",
|
"HUR",
|
||||||
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
||||||
);
|
);
|
||||||
if (timeResult.type === "success") {
|
if (timeResult.success) {
|
||||||
timeSuccess = true;
|
timeSuccess = true;
|
||||||
} else {
|
} else {
|
||||||
logger.error("Error sending time:", timeResult);
|
logger.error("Error sending time:", timeResult);
|
||||||
@@ -154,7 +154,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
timeResult?.error?.userMessage ||
|
timeResult?.error ||
|
||||||
"There was an error sending the time.",
|
"There was an error sending the time.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
@@ -171,7 +171,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
apiServer,
|
apiServer,
|
||||||
axios,
|
axios,
|
||||||
);
|
);
|
||||||
if (attendResult.type === "success") {
|
if (attendResult.success) {
|
||||||
attendedSuccess = true;
|
attendedSuccess = true;
|
||||||
} else {
|
} else {
|
||||||
logger.error("Error sending attendance:", attendResult);
|
logger.error("Error sending attendance:", attendResult);
|
||||||
@@ -181,7 +181,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
attendResult?.error?.userMessage ||
|
attendResult?.error ||
|
||||||
"There was an error sending the attendance.",
|
"There was an error sending the attendance.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
|
text: "Your contact info was copied to the clipboard. Have them click on it, or paste it in the box on their 'Contacts' screen.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
</button>
|
</button>
|
||||||
Individual Profile
|
Individual Profile
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="text-sm text-center text-slate-500"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
@@ -32,6 +33,12 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<font-awesome icon="user" class="fa-fw text-slate-400"></font-awesome>
|
<font-awesome icon="user" class="fa-fw text-slate-400"></font-awesome>
|
||||||
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }}
|
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
<button title="Copy Link to Profile" @click="onCopyLinkClick()">
|
||||||
|
<font-awesome
|
||||||
|
icon="link"
|
||||||
|
class="text-sm text-slate-500 ml-2 mb-1"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="profile.description" class="mt-4 text-slate-600">
|
<p v-if="profile.description" class="mt-4 text-slate-600">
|
||||||
{{ profile.description }}
|
{{ profile.description }}
|
||||||
@@ -100,6 +107,7 @@ import { Router, RouteLocationNormalizedLoaded } 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 {
|
import {
|
||||||
|
APP_SERVER,
|
||||||
DEFAULT_PARTNER_API_SERVER,
|
DEFAULT_PARTNER_API_SERVER,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
USE_DEXIE_DB,
|
USE_DEXIE_DB,
|
||||||
@@ -113,6 +121,7 @@ import { retrieveAccountDids } from "../libs/util";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { Settings } from "@/db/tables/settings";
|
import { Settings } from "@/db/tables/settings";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LMap,
|
LMap,
|
||||||
@@ -186,6 +195,10 @@ export default class UserProfileView extends Vue {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
this.profile = result.data;
|
this.profile = result.data;
|
||||||
|
if (this.profile && this.profile.rowId !== profileId) {
|
||||||
|
// currently the server returns "rowid" with lowercase "i"; remove when that's fixed
|
||||||
|
this.profile.rowId = profileId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Failed to load profile");
|
throw new Error("Failed to load profile");
|
||||||
}
|
}
|
||||||
@@ -204,5 +217,22 @@ export default class UserProfileView extends Vue {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopyLinkClick() {
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
|
useClipboard()
|
||||||
|
.copy(deepLink)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Copied",
|
||||||
|
text: "A link to this profile was copied to the clipboard.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ test('Record something given', async ({ page }) => {
|
|||||||
// Refresh home view and check gift
|
// Refresh home view and check gift
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
const item = await page.locator('li').filter({ hasText: finalTitle });
|
const item = await page.locator('li').filter({ hasText: finalTitle });
|
||||||
await item.locator('[data-testid="circle-info-link"]').click();
|
await item.getByTestId('circle-info-link').first().click();
|
||||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||||
const page1Promise = page.waitForEvent('popup');
|
const page1Promise = page.waitForEvent('popup');
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ test('Record item given from image-share', async ({ page }) => {
|
|||||||
|
|
||||||
// Refresh home view and check gift
|
// Refresh home view and check gift
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
const item1 = page.locator('li').filter({ hasText: finalTitle });
|
const item1 = page.locator('li').filter({ hasText: finalTitle }).first();
|
||||||
await expect(item1.getByRole('img')).toBeVisible();
|
await expect(item1.getByRole('img')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
await expect(page.locator('li.border-b')).toContainText(userName);
|
await expect(page.locator('li.border-b')).toContainText(userName);
|
||||||
|
|
||||||
// Rename contact
|
// Rename contact
|
||||||
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${userName}") + span svg.fa-circle-info`).click();
|
await page.getByTestId(`contactListItem`).locator(`h2 a:has-text("${userName}")`).click();
|
||||||
// now on the DID view page
|
// now on the DID view page
|
||||||
await page.locator('h2 svg.fa-pen').click();
|
await page.locator('h2 svg.fa-pen').click();
|
||||||
// now on the contact edit page
|
// now on the contact edit page
|
||||||
@@ -116,10 +116,11 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
// Confirm that home shows contact in "Record Something…"
|
// Confirm that home shows contact in "Record Something…"
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
await expect(page.locator('#sectionRecordSomethingGiven ul li').filter({ hasText: contactName }).nth(0)).toBeVisible();
|
const userGaveLink = page.locator('#sectionRecordSomethingGiven ul li').filter({ hasText: contactName }).nth(0);
|
||||||
|
await expect(userGaveLink).toBeVisible();
|
||||||
|
await userGaveLink.click();
|
||||||
|
|
||||||
// Record something given by new contact
|
// Record something given by new contact
|
||||||
await page.getByRole('heading', { name: contactName }).click();
|
|
||||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
await page.getByPlaceholder('What was given').fill(finalTitle);
|
||||||
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
await page.getByRole('spinbutton').fill(randomNonZeroNumber.toString());
|
||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
@@ -130,7 +131,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
|
|
||||||
// Firefox complains on load the initial feed here when we use the test server.
|
// Firefox complains on load the initial feed here when we use the test server.
|
||||||
// It may be similar to the CORS problem below.
|
// It may be similar to the CORS problem below.
|
||||||
await page.locator('li').filter({ hasText: finalTitle }).locator('a').click();
|
await page.locator('li').filter({ hasText: finalTitle }).getByTestId('circle-info-link').click();
|
||||||
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Verifiable Claim Details' })).toBeVisible();
|
||||||
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
await expect(page.getByText(finalTitle, { exact: true })).toBeVisible();
|
||||||
|
|
||||||
@@ -154,7 +155,7 @@ test('Add contact, record gift, confirm gift', async ({ page }) => {
|
|||||||
// Go to home view and look for gift
|
// Go to home view and look for gift
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
const giftLink = page.locator('li').filter({ hasText: finalTitle }).locator('a');
|
const giftLink = page.locator('li').filter({ hasText: finalTitle }).getByTestId('circle-info-link').first();
|
||||||
await expect(giftLink).toBeVisible();
|
await expect(giftLink).toBeVisible();
|
||||||
await giftLink.click();
|
await giftLink.click();
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ test('Add contact, copy details, delete, and import from paste & from file', asy
|
|||||||
// See a different clipboard solution below.
|
// See a different clipboard solution below.
|
||||||
|
|
||||||
// see contact details on the second contact
|
// see contact details on the second contact
|
||||||
await page.getByTestId('contactListItem').nth(1).locator('a').click();
|
await page.getByTestId('contactListItem').nth(1).locator(`h2 a)`).click();
|
||||||
await page.getByRole('heading', { name: 'Identifier Details' }).isVisible();
|
await page.getByRole('heading', { name: 'Identifier Details' }).isVisible();
|
||||||
// remove contact
|
// remove contact
|
||||||
await page.locator('button > svg.fa-trash-can').click();
|
await page.locator('button > svg.fa-trash-can').click();
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export async function deleteContact(page: Page, did: string): Promise<void> {
|
|||||||
await page.goto('./contacts');
|
await page.goto('./contacts');
|
||||||
const contactName = createContactName(did);
|
const contactName = createContactName(did);
|
||||||
// go to the detail page for this contact
|
// go to the detail page for this contact
|
||||||
await page.locator(`li[data-testid="contactListItem"] h2:has-text("${contactName}") + span svg.fa-circle-info`).click();
|
await page.getByTestId(`contactListItem`).locator(`h2 a:has-text("${contactName}")`).click();
|
||||||
// delete the contact
|
// delete the contact
|
||||||
await page.locator('button > svg.fa-trash-can').click();
|
await page.locator('button > svg.fa-trash-can').click();
|
||||||
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
await page.locator('div[role="alert"] button:has-text("Yes")').click();
|
||||||
@@ -82,7 +82,7 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
|
|||||||
return newDid;
|
return newDid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a new random user and register them.
|
// Generate a new random user, register them, and add them as a contact with name from createContactName.
|
||||||
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
||||||
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
||||||
const newDid = await generateNewEthrUser(page);
|
const newDid = await generateNewEthrUser(page);
|
||||||
@@ -109,7 +109,7 @@ export async function generateRandomString(length: number): Promise<string> {
|
|||||||
|
|
||||||
// Function to create an array of unique strings
|
// Function to create an array of unique strings
|
||||||
export async function createUniqueStringsArray(count: number): Promise<string[]> {
|
export async function createUniqueStringsArray(count: number): Promise<string[]> {
|
||||||
const stringsArray = [];
|
const stringsArray: string[] = [];
|
||||||
const stringLength = 16;
|
const stringLength = 16;
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
@@ -122,7 +122,7 @@ export async function createUniqueStringsArray(count: number): Promise<string[]>
|
|||||||
|
|
||||||
// Function to create an array of two-digit non-zero numbers
|
// Function to create an array of two-digit non-zero numbers
|
||||||
export async function createRandomNumbersArray(count: number): Promise<number[]> {
|
export async function createRandomNumbersArray(count: number): Promise<number[]> {
|
||||||
const numbersArray = [];
|
const numbersArray: number[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let randomNumber = Math.floor(Math.random() * 99) + 1;
|
let randomNumber = Math.floor(Math.random() * 99) + 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user