Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3118f71320 | |||
| d12f23aa81 | |||
| e9a8a3c1e7 | |||
| 1e0efe6011 | |||
| 16557f1e4b | |||
| c4a54967bc | |||
| 20ade415dc | |||
| 6689520270 | |||
| 3fd6c2b80d |
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -64,6 +65,8 @@ Install dependencies:
|
|||||||
|
|
||||||
* Commit everything (since the commit hash is used the app).
|
* Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
|
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
|
||||||
|
|
||||||
* 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 later).
|
||||||
|
|
||||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
|
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
|
||||||
@@ -71,7 +74,7 @@ Install dependencies:
|
|||||||
* 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):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
... and transfer to the test server:
|
... and transfer to the test server:
|
||||||
@@ -360,10 +363,10 @@ Prerequisites: macOS with Xcode installed
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App
|
cd ios/App
|
||||||
xcrun agvtool new-version 33
|
xcrun agvtool new-version 34
|
||||||
# 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
|
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj
|
||||||
cd -
|
cd -
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ 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).
|
||||||
|
|
||||||
|
|
||||||
|
## [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]
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -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 34
|
||||||
versionName "0.5.7"
|
versionName "0.5.8"
|
||||||
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.
|
||||||
|
|||||||
@@ -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 = 34;
|
||||||
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 = 0.5.8;
|
||||||
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 = 34;
|
||||||
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 = 0.5.8;
|
||||||
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 = "";
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.5.4",
|
"version": "0.5.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.5.4",
|
"version": "0.5.8",
|
||||||
"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.6",
|
"version": "0.5.8",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
@@ -67,8 +67,9 @@ export const logger = {
|
|||||||
// 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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -519,7 +519,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 +551,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}`,
|
||||||
@@ -581,9 +577,6 @@ 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();
|
||||||
}
|
}
|
||||||
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}`,
|
||||||
@@ -641,9 +634,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 +675,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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user