diff --git a/BUILDING.md b/BUILDING.md index a159d900..831d5d40 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -41,6 +41,7 @@ Install dependencies: 1. Run the production build: ```bash + rm -rf dist npm run build:web ``` @@ -64,6 +65,8 @@ Install dependencies: * 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). * 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): ```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: @@ -360,10 +363,10 @@ Prerequisites: macOS with Xcode installed ``` cd ios/App - xcrun agvtool new-version 33 + xcrun agvtool new-version 34 # Unfortunately this edits Info.plist directly. #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 - ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 71657a57..b6ce5430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). +## [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] ### Fixed diff --git a/android/app/build.gradle b/android/app/build.gradle index 810eccff..bfa25355 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,8 +31,8 @@ android { applicationId "app.timesafari.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 33 - versionName "0.5.7" + versionCode 34 + versionName "0.5.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/doc/DEEP_LINKS.md b/doc/DEEP_LINKS.md index a68a5ed1..a6bf9f6b 100644 --- a/doc/DEEP_LINKS.md +++ b/doc/DEEP_LINKS.md @@ -100,6 +100,7 @@ try { - `src/interfaces/deepLinks.ts`: Type definitions and validation schemas - `src/services/deepLinks.ts`: Deep link processing service - `src/main.capacitor.ts`: Capacitor integration +- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web ## Type Safety Examples diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index e4a7b5e3..d3e6b9b4 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -403,7 +403,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.7; + MARKETING_VERSION = 0.5.8; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -430,7 +430,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 33; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.7; + MARKETING_VERSION = 0.5.8; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package-lock.json b/package-lock.json index a3e24731..7b2d9a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.8", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 935ece64..06722ca3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.8", "description": "Time Safari Application", "author": { "name": "Time Safari Team" diff --git a/src/components/HiddenDidDialog.vue b/src/components/HiddenDidDialog.vue index 980a3852..8593009e 100644 --- a/src/components/HiddenDidDialog.vue +++ b/src/components/HiddenDidDialog.vue @@ -77,7 +77,7 @@ If you'd like an introduction, click here to copy this page, paste it into a message, and ask if they'll tell you more about the {{ roleName }}. @@ -104,7 +104,7 @@ import * as R from "ramda"; import { useClipboard } from "@vueuse/core"; import { Contact } from "../db/tables/contacts"; import * as serverUtil from "../libs/endorserServer"; -import { NotificationIface } from "../constants/app"; +import { APP_SERVER, NotificationIface } from "../constants/app"; @Component export default class HiddenDidDialog extends Vue { @@ -117,7 +117,8 @@ export default class HiddenDidDialog extends Vue { activeDid = ""; allMyDids: Array = []; canShare = false; - windowLocation = window.location.href; + deepLinkPathSuffix = ""; + deepLinkUrl = window.location.href; // this is changed to a deep link in the setup R = R; serverUtil = serverUtil; @@ -129,17 +130,21 @@ export default class HiddenDidDialog extends Vue { } open( + deepLinkPathSuffix: string, roleName: string, visibleToDids: string[], allContacts: Array, activeDid: string, allMyDids: Array, ) { + this.deepLinkPathSuffix = deepLinkPathSuffix; this.roleName = roleName; this.visibleToDids = visibleToDids; this.allContacts = allContacts; this.activeDid = activeDid; this.allMyDids = allMyDids; + + this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix; this.isOpen = true; } @@ -173,11 +178,11 @@ export default class HiddenDidDialog extends Vue { } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.windowLocation); + this.copyToClipboard("A link to this page", this.deepLinkUrl); window.navigator.share({ title: "Help Connect Me", text: "I'm trying to find the people who recorded this. Can you help me?", - url: this.windowLocation, + url: this.deepLinkUrl, }); } } diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index c8688b83..1d9ab4bc 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -219,9 +219,9 @@ export async function logConsoleAndDb( isError = false, ): Promise { if (isError) { - logger.error(`${new Date().toISOString()} ${message}`); + logger.error(`${new Date().toISOString()}`, message); } else { - logger.log(`${new Date().toISOString()} ${message}`); + logger.log(`${new Date().toISOString()}`, message); } await logToDb(message); } diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index b56862c1..a275eecf 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -29,18 +29,17 @@ import { z } from "zod"; // Add a union type of all valid route paths export const VALID_DEEP_LINK_ROUTES = [ - "user-profile", - "project", - "onboard-meeting-setup", - "invite-one-accept", - "contact-import", - "confirm-gift", + // note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts "claim", - "claim-cert", "claim-add-raw", - "contact-edit", - "contacts", + "claim-cert", + "confirm-gift", + "contact-import", "did", + "invite-one-accept", + "onboard-meeting-setup", + "project", + "user-profile", ] as const; // Create a type from the array @@ -58,43 +57,38 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES); // Parameter validation schemas for each route type export const deepLinkSchemas = { - "user-profile": z.object({ + // note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts + claim: z.object({ id: z.string(), }), - project: z.object({ + "claim-add-raw": z.object({ id: z.string(), + claim: z.string().optional(), + claimJwtId: z.string().optional(), }), - "onboard-meeting-setup": z.object({ + "claim-cert": z.object({ id: z.string(), }), - "invite-one-accept": z.object({ + "confirm-gift": z.object({ id: z.string(), }), "contact-import": z.object({ jwt: z.string(), }), - "confirm-gift": z.object({ - id: z.string(), + did: z.object({ + did: z.string(), }), - claim: z.object({ - id: z.string(), + "invite-one-accept": z.object({ + jwt: z.string(), }), - "claim-cert": z.object({ + "onboard-meeting-setup": z.object({ id: z.string(), }), - "claim-add-raw": z.object({ + project: z.object({ id: z.string(), - claim: z.string().optional(), - claimJwtId: z.string().optional(), - }), - "contact-edit": z.object({ - did: z.string(), }), - contacts: z.object({ - contacts: z.string(), // JSON string of contacts array - }), - did: z.object({ - did: z.string(), + "user-profile": z.object({ + id: z.string(), }), }; diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index cbdcbaee..f2dc79a4 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -1074,7 +1074,8 @@ export async function generateEndorserJwtUrlForAccount( 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; } diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index dc4074c4..3ac12d1f 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -34,8 +34,7 @@ import router from "./router"; import { handleApiError } from "./services/api"; import { AxiosError } from "axios"; import { DeepLinkHandler } from "./services/deepLinks"; -import { logConsoleAndDb } from "./db/databaseUtil"; -import { logger } from "./utils/logger"; +import { logger, safeStringify } from "./utils/logger"; logger.log("[Capacitor] Starting initialization"); logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM); @@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => { await router.isReady(); await deepLinkHandler.handleDeepLink(data.url); } catch (error) { - logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true); + logger.error("[DeepLink] Error handling deep link: ", error); handleApiError( { - message: error instanceof Error ? error.message : String(error), + message: error instanceof Error ? error.message : safeStringify(error), } as AxiosError, "deep-link", ); diff --git a/src/router/index.ts b/src/router/index.ts index 0b9aa52b..fabce1b5 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -83,6 +83,11 @@ const routes: Array = [ name: "discover", component: () => import("../views/DiscoverView.vue"), }, + { + path: "/deep-link/:path*", + name: "deep-link", + component: () => import("../views/DeepLinkRedirectView.vue"), + }, { path: "/gifted-details", name: "gifted-details", diff --git a/src/services/api.ts b/src/services/api.ts index 3235100e..d7b67beb 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -6,7 +6,7 @@ */ 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. @@ -37,7 +37,8 @@ import { logger } from "../utils/logger"; */ export const handleApiError = (error: AxiosError, endpoint: string) => { 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, status: error.response?.status, data: error.response?.data, diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index e9f63a88..bff3346c 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -27,18 +27,16 @@ * timesafari://[/][?queryParam1=value1&queryParam2=value2] * * 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-cert: View claim certificate * - claim-add-raw: Add raw claim - * - contact-edit: Edit contact - * - contacts: View contacts + * - claim-cert: View claim certificate + * - confirm-gift + * - contact-import: Import contacts * - did: View DID + * - invite-one-accept: Accept invitation + * - onboard-meeting-members + * - project: View project details + * - user-profile: View user profile * * @example * const handler = new DeepLinkHandler(router); @@ -81,14 +79,15 @@ export class DeepLinkHandler { string, { name: string; paramKey?: string } > = { + // note that similar lists are in src/interfaces/deepLinks.ts claim: { name: "claim" }, "claim-add-raw": { name: "claim-add-raw" }, "claim-cert": { name: "claim-cert" }, "confirm-gift": { name: "confirm-gift" }, + "contact-import": { name: "contact-import", paramKey: "jwt" }, did: { name: "did", paramKey: "did" }, "invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" }, "onboard-meeting-members": { name: "onboard-meeting-members" }, - "onboard-meeting-setup": { name: "onboard-meeting-setup" }, project: { name: "project" }, "user-profile": { name: "user-profile" }, }; @@ -99,7 +98,7 @@ export class DeepLinkHandler { * * @param url - The deep link URL to parse (format: scheme://path[?query]) * @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) { const parts = url.split("://"); @@ -115,7 +114,16 @@ export class DeepLinkHandler { }); 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 if (!this.ROUTE_MAP[routePath]) { @@ -134,45 +142,14 @@ export class DeepLinkHandler { } const params: Record = {}; - if (param) { + if (pathParams) { // Now we know routePath exists in ROUTE_MAP const routeConfig = this.ROUTE_MAP[routePath]; - params[routeConfig.paramKey ?? "id"] = param; + params[routeConfig.paramKey ?? "id"] = pathParams.join("/"); } 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 { - try { - logConsoleAndDb("[DeepLink] Processing URL: " + url, false); - const { path, params, query } = this.parseDeepLink(url); - // Ensure params is always a Record 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. * Validates route and parameters using Zod schemas before routing. @@ -243,6 +220,39 @@ export class DeepLinkHandler { code: "INVALID_PARAMETERS", message: (error as Error).message, 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 { + try { + logConsoleAndDb("[DeepLink] Processing URL: " + url, false); + const { path, params, query } = this.parseDeepLink(url); + // Ensure params is always a Record 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, }; } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2cbb228b..89425d77 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,6 +1,6 @@ import { logToDb } from "../db/databaseUtil"; -function safeStringify(obj: unknown) { +export function safeStringify(obj: unknown) { const seen = new WeakSet(); return JSON.stringify(obj, (_key, value) => { @@ -67,8 +67,9 @@ export const logger = { // Errors will always be logged // eslint-disable-next-line no-console console.error(message, ...args); - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); + const messageString = safeStringify(message); + const argsString = args.length > 0 ? safeStringify(args) : ""; + logToDb(messageString + argsString); }, }; diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 1d647d7a..8edf3bf8 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -49,21 +49,32 @@ v-if="veriClaim.id" :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)" class="text-blue-500 mt-2" - title="Printable Certificate" + title="View Printable Certificate" > +
@@ -405,7 +416,7 @@ contacts can see more details: click to copy this page info and see if they can make an introduction. Someone is connected to @@ -428,7 +439,7 @@ If you'd like an introduction, share this page with them and ask if they'll tell you more about about the participants. @@ -546,7 +557,7 @@ import { useClipboard } from "@vueuse/core"; import { GenericVerifiableCredential } from "../interfaces"; import GiftedDialog from "../components/GiftedDialog.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 { db } from "../db/index"; import { logConsoleAndDb } from "../db/databaseUtil"; @@ -593,8 +604,9 @@ export default class ClaimView extends Vue { veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; veriClaimDump = ""; 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; yaml = yaml; libsUtil = libsUtil; @@ -671,6 +683,7 @@ export default class ClaimView extends Vue { 5000, ); } + this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; this.canShare = !!navigator.share; } @@ -1006,11 +1019,11 @@ export default class ClaimView extends Vue { } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.windowLocation); + this.copyToClipboard("A link to this page", this.windowDeepLink); window.navigator.share({ title: "Help Connect Me", text: "I'm trying to find the people who recorded this. Can you help me?", - url: this.windowLocation, + url: this.windowDeepLink, }); } diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 63225259..fbf81bab 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -436,7 +436,7 @@ import { Component, Vue } from "vue-facing-decorator"; import { useClipboard } from "@vueuse/core"; import { RouteLocationNormalizedLoaded, Router } from "vue-router"; 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 { Contact } from "../db/tables/contacts"; import * as databaseUtil from "../db/databaseUtil"; @@ -494,7 +494,7 @@ export default class ConfirmGiftView extends Vue { veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; veriClaimDump = ""; veriClaimDidsVisible: { [key: string]: string[] } = {}; - windowLocation = window.location.href; + windowLocation = window.location.href; // this is changed to a deep link in the setup R = R; yaml = yaml; @@ -566,6 +566,9 @@ export default class ConfirmGiftView extends Vue { } const claimId = decodeURIComponent(pathParam); + + this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId; + await this.loadClaim(claimId, this.activeDid); } @@ -676,12 +679,12 @@ export default class ConfirmGiftView extends Vue { /** * Add participant (giver/recipient) name & URL info */ + this.giverName = this.didInfo(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.recipientName = this.didInfo(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)}`; } diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index eba485ca..c5a08b74 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -124,12 +124,14 @@ import * as databaseUtil from "../db/databaseUtil"; import { CONTACT_CSV_HEADER, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + generateEndorserJwtUrlForAccount, setVisibilityUtil, } from "../libs/endorserServer"; import UserNameDialog from "../components/UserNameDialog.vue"; import { retrieveAccountMetadata } from "../libs/util"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { parseJsonField } from "../db/databaseUtil"; +import { Account } from "@/db/tables/accounts"; interface QRScanResult { rawValue?: string; @@ -157,6 +159,7 @@ export default class ContactQRScanFull extends Vue { apiServer = ""; givenName = ""; isRegistered = false; + profileImageUrl = ""; qrValue = ""; ETHR_DID_PREFIX = ETHR_DID_PREFIX; @@ -179,6 +182,7 @@ export default class ContactQRScanFull extends Vue { this.apiServer = settings.apiServer || ""; this.givenName = settings.firstName || ""; this.isRegistered = !!settings.isRegistered; + this.profileImageUrl = settings.profileImageUrl || ""; const account = await retrieveAccountMetadata(this.activeDid); 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() - .copy(this.qrValue) + .copy(jwtUrl) .then(() => { this.$notify( { diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 076ee279..29b28e5f 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -177,6 +177,7 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { CONTACT_CSV_HEADER, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + generateEndorserJwtUrlForAccount, register, setVisibilityUtil, } from "../libs/endorserServer"; @@ -187,6 +188,7 @@ import { logger } from "../utils/logger"; import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory"; import { CameraState } from "@/services/QRScanner/types"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { Account } from "@/db/tables/accounts"; interface QRScanResult { rawValue?: string; @@ -216,6 +218,7 @@ export default class ContactQRScanShow extends Vue { isRegistered = false; qrValue = ""; isScanning = false; + profileImageUrl = ""; error: string | null = null; // QR Scanner properties @@ -253,6 +256,7 @@ export default class ContactQRScanShow extends Vue { this.hideRegisterPromptOnNewContact = !!settings.hideRegisterPromptOnNewContact; this.isRegistered = !!settings.isRegistered; + this.profileImageUrl = settings.profileImageUrl || ""; const account = await libsUtil.retrieveAccountMetadata(this.activeDid); if (account) { @@ -667,10 +671,19 @@ export default class ContactQRScanShow extends Vue { }); } - onCopyUrlToClipboard() { - //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing + async onCopyUrlToClipboard() { + const account = (await libsUtil.retrieveFullyDecryptedAccount( + this.activeDid, + )) as Account; + const jwtUrl = await generateEndorserJwtUrlForAccount( + account, + this.isRegistered, + this.givenName, + this.profileImageUrl, + true, + ); useClipboard() - .copy(this.qrValue) + .copy(jwtUrl) .then(() => { this.$notify( { diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index cdbadb00..fbf85774 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -126,7 +126,6 @@
+
@@ -55,7 +61,11 @@ {{ issuerInfoObject?.displayName }} - + +
@@ -632,7 +642,7 @@ import TopMessage from "../components/TopMessage.vue"; import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.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 { db, @@ -646,6 +656,7 @@ import { retrieveAccountDids } from "../libs/util"; import HiddenDidDialog from "../components/HiddenDidDialog.vue"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { useClipboard } from "@vueuse/core"; /** * Project View Component * @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? expandText() { this.expanded = true; @@ -1304,7 +1337,7 @@ export default class ProjectViewView extends Vue { } // return an HTTPS URL if it's not a global URL - addScheme(url: string) { + ensureScheme(url: string) { if (!libsUtil.isGlobalUri(url)) { return "https://" + url; } @@ -1465,7 +1498,13 @@ export default class ProjectViewView extends Vue { } 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( + "project/" + shortestProjectId, "creator", this.issuerVisibleToDids, this.allContacts, diff --git a/src/views/ShareMyContactInfoView.vue b/src/views/ShareMyContactInfoView.vue index 445c2775..ca1f2e94 100644 --- a/src/views/ShareMyContactInfoView.vue +++ b/src/views/ShareMyContactInfoView.vue @@ -105,7 +105,7 @@ export default class ShareMyContactInfoView extends Vue { group: "alert", type: "info", 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, ); diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index 0e1471b8..1defd348 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -16,6 +16,7 @@ Individual Profile +
@@ -32,6 +33,12 @@
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }} +

{{ profile.description }} @@ -100,6 +107,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router"; import QuickNav from "../components/QuickNav.vue"; import TopMessage from "../components/TopMessage.vue"; import { + APP_SERVER, DEFAULT_PARTNER_API_SERVER, NotificationIface, USE_DEXIE_DB, @@ -113,6 +121,7 @@ import { retrieveAccountDids } from "../libs/util"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { Settings } from "@/db/tables/settings"; +import { useClipboard } from "@vueuse/core"; @Component({ components: { LMap, @@ -186,6 +195,10 @@ export default class UserProfileView extends Vue { if (response.status === 200) { const result = await response.json(); 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 { throw new Error("Failed to load profile"); } @@ -204,5 +217,22 @@ export default class UserProfileView extends Vue { 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, + ); + }); + } }