Implement configurable domain names for all copy link functionality

- Add PROD_SHARE_DOMAIN constant using existing AppString.PROD_PUSH_SERVER
- Update all 9 components/services to use configurable domain instead of hardcoded URLs
- Fix localhost issues in development mode for all sharing functionality
- Ensure all copy link buttons generate production URLs regardless of environment
- Add proper TypeScript imports and component properties for template access
- Maintain existing functionality while improving maintainability and consistency

Files updated:
- src/constants/app.ts (new constant)
- src/views/ClaimView.vue (claim + certificate links)
- src/views/ProjectViewView.vue (project links)
- src/views/ConfirmGiftView.vue (confirm gift links)
- src/components/HiddenDidDialog.vue (hidden DID links)
- src/views/UserProfileView.vue (profile links)
- src/views/InviteOneView.vue (invite links)
- src/views/ContactsView.vue (contact import links)
- src/views/OnboardMeetingSetupView.vue (meeting links)
- src/libs/endorserServer.ts (contact import confirm links)

Documentation added:
- docs/domain-configuration.md (comprehensive guide)
- README.md (quick reference section)

Security audit:  All changes maintain existing security model
Testing:  All linting errors resolved, only warnings remain
Performance:  No performance impact, improves user experience
This commit is contained in:
Matthew Raymer
2025-07-21 07:47:34 +00:00
parent 29e6d5e099
commit 7ab595cb60
12 changed files with 310 additions and 32 deletions

View File

@@ -113,14 +113,15 @@
* @since 2024-12-19
*/
import { Component, Vue } from "vue-facing-decorator";
import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { Contact } from "../db/tables/contacts";
import * as R from "ramda";
import * as serverUtil from "../libs/endorserServer";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { NotificationIface } from "../constants/app";
import { createNotifyHelpers } from "@/utils/notify";
import { TIMEOUTS } from "@/utils/notify";
import { NOTIFY_COPIED_TO_CLIPBOARD } from "@/constants/notifications";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
@Component({ name: "HiddenDidDialog" })
export default class HiddenDidDialog extends Vue {
@@ -139,6 +140,7 @@ export default class HiddenDidDialog extends Vue {
R = R;
serverUtil = serverUtil;
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
// =================================================
// COMPUTED PROPERTIES - Template Streamlining
@@ -180,7 +182,8 @@ export default class HiddenDidDialog extends Vue {
this.activeDid = activeDid;
this.allMyDids = allMyDids;
this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
// Use production URL for sharing to avoid localhost issues in development
this.deepLinkUrl = `${PROD_SHARE_DOMAIN}/deep-link/${this.deepLinkPathSuffix}`;
this.isOpen = true;
}

View File

@@ -52,6 +52,9 @@ export const DEFAULT_PARTNER_API_SERVER =
export const DEFAULT_PUSH_SERVER =
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
// Production domain for sharing links (always use production URL for sharing)
export const PROD_SHARE_DOMAIN = AppString.PROD_PUSH_SERVER;
export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =

View File

@@ -22,11 +22,7 @@ import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache";
import * as R from "ramda";
import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
APP_SERVER,
} from "../constants/app";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { NOTIFICATION_TIMEOUTS } from "../composables/useNotifications";
import { createNotifyHelpers } from "../utils/notify";
import { NOTIFY_PERSONAL_DATA_ERROR } from "../constants/notifications";
@@ -63,6 +59,7 @@ import {
import { PlanSummaryRecord } from "../interfaces/records";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
/**
* Standard context for schema.org data
@@ -1086,8 +1083,8 @@ export async function generateEndorserJwtUrlForAccount(
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
const viewPrefix =
APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
// Use production URL for sharing to avoid localhost issues in development
const viewPrefix = `${PROD_SHARE_DOMAIN}/deep-link${CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI}`;
return viewPrefix + vcJwt;
}

View File

@@ -58,7 +58,7 @@
@click="
copyToClipboard(
'A link to the certificate page',
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
`${PROD_SHARE_DOMAIN}/deep-link/claim-cert/${veriClaim.id}`,
)
"
>
@@ -532,6 +532,7 @@ import {
import * as libsUtil from "../libs/util";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
@Component({
components: { GiftedDialog, QuickNav },
@@ -577,6 +578,7 @@ export default class ClaimView extends Vue {
yaml = yaml;
libsUtil = libsUtil;
serverUtil = serverUtil;
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
notify!: ReturnType<typeof createNotifyHelpers>;
@@ -742,7 +744,8 @@ export default class ClaimView extends Vue {
} else {
this.notify.error("No claim ID was provided.");
}
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
// Use production URL for sharing to avoid localhost issues in development
this.windowDeepLink = `${PROD_SHARE_DOMAIN}/deep-link/claim/${claimId}`;
this.canShare = !!navigator.share;

View File

@@ -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 { APP_SERVER, NotificationIface } from "../constants/app";
import { NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import * as serverUtil from "../libs/endorserServer";
import { GenericVerifiableCredential, GiveSummaryRecord } from "../interfaces";
@@ -447,6 +447,7 @@ import TopMessage from "../components/TopMessage.vue";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
import {
NOTIFY_GIFT_ERROR_LOADING,
NOTIFY_GIFT_CONFIRMATION_SUCCESS,
@@ -510,6 +511,7 @@ export default class ConfirmGiftView extends Vue {
libsUtil = libsUtil;
serverUtil = serverUtil;
displayAmount = displayAmount;
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
/**
* Component lifecycle hook that initializes notification helpers
@@ -570,7 +572,8 @@ export default class ConfirmGiftView extends Vue {
const claimId = decodeURIComponent(pathParam);
this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId;
// Use production URL for sharing to avoid localhost issues in development
this.windowLocation = `${PROD_SHARE_DOMAIN}/deep-link/confirm-gift/${claimId}`;
await this.loadClaim(claimId, this.activeDid);
}

View File

@@ -167,6 +167,7 @@ import { logger } from "../utils/logger";
// import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
import {
NOTIFY_CONTACT_NO_INFO,
NOTIFY_CONTACTS_ADD_ERROR,
@@ -274,6 +275,7 @@ export default class ContactsView extends Vue {
APP_SERVER = APP_SERVER;
AppString = AppString;
libsUtil = libsUtil;
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
/**
* Component lifecycle hook - Initialize component state and load data
@@ -1168,8 +1170,8 @@ export default class ContactsView extends Vue {
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
contacts: selectedContacts,
});
const contactsJwtUrl =
APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
// Use production URL for sharing to avoid localhost issues in development
const contactsJwtUrl = `${PROD_SHARE_DOMAIN}/deep-link/contact-import/${contactsJwt}`;
useClipboard()
.copy(contactsJwtUrl)
.then(() => {

View File

@@ -135,12 +135,13 @@ import ContactNameDialog from "../components/ContactNameDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import InviteDialog from "../components/InviteDialog.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { createInviteJwt, getHeaders } from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
import {
NOTIFY_INVITE_LOAD_ERROR,
NOTIFY_INVITE_DELETED,
@@ -197,6 +198,8 @@ export default class InviteOneView extends Vue {
showAppleWarning = false;
notify!: ReturnType<typeof createNotifyHelpers>;
/** Production share domain for deep links */
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
/**
* Initializes notification helpers
@@ -326,7 +329,8 @@ export default class InviteOneView extends Vue {
}
inviteLink(jwt: string): string {
return APP_SERVER + "/deep-link/invite-one-accept/" + jwt;
// Use production URL for sharing to avoid localhost issues in development
return `${PROD_SHARE_DOMAIN}/deep-link/invite-one-accept/${jwt}`;
}
copyInviteAndNotify(inviteId: string, jwt: string) {

View File

@@ -282,9 +282,9 @@ import {
serverMessageForUser,
} from "../libs/endorserServer";
import { encryptMessage } from "../libs/crypto";
import { APP_SERVER } from "@/constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
import {
NOTIFY_MEETING_INVALID_TIME,
NOTIFY_MEETING_NAME_REQUIRED,
@@ -326,6 +326,8 @@ export default class OnboardMeetingView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
/** Production share domain for deep links */
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
currentMeeting: ServerMeeting | null = null;
newOrUpdatedMeetingInputs: MeetingSetupInputs | null = null;
@@ -660,7 +662,8 @@ export default class OnboardMeetingView extends Vue {
onboardMeetingMembersLink(): string {
if (this.currentMeeting) {
return `${APP_SERVER}/deep-link/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
// Use production URL for sharing to avoid localhost issues in development
return `${PROD_SHARE_DOMAIN}/deep-link/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
this.currentMeeting?.password || "",
)}`;
}

View File

@@ -595,7 +595,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 { APP_SERVER, NotificationIface } from "../constants/app";
import { NotificationIface } from "../constants/app";
// Removed legacy logging import - migrated to PlatformServiceMixin
import { Contact } from "../db/tables/contacts";
import * as libsUtil from "../libs/util";
@@ -607,6 +607,7 @@ import { useClipboard } from "@vueuse/core";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
import { PROD_SHARE_DOMAIN } from "@/constants/app";
/**
* Project View Component
* @author Matthew Raymer
@@ -739,6 +740,8 @@ export default class ProjectViewView extends Vue {
// Utility References
libsUtil = libsUtil;
serverUtil = serverUtil;
/** Production share domain for deep links */
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
/**
* Component lifecycle hook that initializes the project view
@@ -799,7 +802,8 @@ export default class ProjectViewView extends Vue {
)
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
: this.projectId;
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
// Use production URL for sharing to avoid localhost issues in development
const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/project/${shortestProjectId}`;
useClipboard()
.copy(deepLink)
.then(() => {

View File

@@ -99,9 +99,9 @@ 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,
PROD_SHARE_DOMAIN,
} from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { didInfo, getHeaders } from "../libs/endorserServer";
@@ -156,6 +156,8 @@ export default class UserProfileView extends Vue {
// make this function available to the Vue template
didInfo = didInfo;
/** Production share domain for deep links */
PROD_SHARE_DOMAIN = PROD_SHARE_DOMAIN;
/**
* Initializes notification helpers
@@ -239,7 +241,8 @@ export default class UserProfileView extends Vue {
* Shows success notification when completed
*/
onCopyLinkClick() {
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
// Use production URL for sharing to avoid localhost issues in development
const deepLink = `${PROD_SHARE_DOMAIN}/deep-link/user-profile/${this.profile?.rowId}`;
useClipboard()
.copy(deepLink)
.then(() => {