Compare commits

...

10 Commits

Author SHA1 Message Date
Jose Olarte III
4c218c4786 feat: migrate all clipboard operations from useClipboard to ClipboardService
- Replace useClipboard with platform-agnostic ClipboardService across 13 files
- Add proper error handling with user notifications for all clipboard operations
- Fix naming conflicts between method names and imported function names
- Ensure consistent async/await patterns throughout the codebase
- Add notification system to HelpView.vue for user feedback on clipboard errors
- Remove unnecessary wrapper methods for cleaner code

Files migrated:
- View components: UserProfileView, QuickActionBvcEndView, ProjectViewView,
  InviteOneView, SeedBackupView, HelpView, AccountViewView, DatabaseMigration,
  ConfirmGiftView, ClaimView, OnboardMeetingSetupView
- Utility functions: libs/util.ts (doCopyTwoSecRedo)
- Components: HiddenDidDialog

Naming conflicts resolved:
- DatabaseMigration: copyToClipboard() → copyExportedDataToClipboard()
- ShareMyContactInfoView: copyToClipboard() → copyContactMessageToClipboard() → removed
- HiddenDidDialog: copyToClipboard() → copyTextToClipboard()
- ClaimView: copyToClipboard() → copyTextToClipboard()
- ConfirmGiftView: copyToClipboard() → copyTextToClipboard()

This migration ensures reliable clipboard functionality across iOS, Android,
and web platforms with proper error handling and user feedback.

Closes: Platform-specific clipboard issues on mobile devices
2025-09-12 14:33:09 +08:00
Jose Olarte III
5fc5b958af fix(ios): resolve clipboard and notification issues in ContactQRScanFullView
- Replace useClipboard() with ClipboardService for iOS compatibility
- Fix notification helper initialization timing issue
- Add proper error handling for clipboard operations
- Ensure consistent behavior across all platforms

Fixes clipboard copy functionality on iOS builds where QR code clicks
failed to copy content and showed notification errors. The ClipboardService
provides platform-specific handling using Capacitor's clipboard plugin,
while moving notification initialization to created() lifecycle hook
prevents undefined function errors.

Resolves: iOS clipboard copy failure and notification system errors
2025-09-10 21:04:36 +08:00
8815f36596 Merge pull request 'refactor: modernize registration prompt notification in ContactQRScanShowView' (#197) from registration-prompt-parity into master
Reviewed-on: #197
2025-09-08 04:38:22 -04:00
631aa468e6 Merge branch 'master' into registration-prompt-parity 2025-09-08 04:37:54 -04:00
ee29b517ce Merge pull request 'feat: implement seed phrase backup reminder system' (#195) from seed-phrase-backup-prompt into master
Reviewed-on: #195
2025-09-08 04:37:33 -04:00
f34c567ab4 Merge branch 'master' into seed-phrase-backup-prompt 2025-09-08 04:37:23 -04:00
bd072d95eb Merge pull request 'Fix offer fulfillment detection + consistencies between ClaimView and ConfirmGiftView' (#167) from claimview-fullfills-offer into master
Reviewed-on: #167
2025-09-08 04:37:03 -04:00
Jose Olarte III
ca1190aa47 refactor: modernize registration prompt notification in ContactQRScanShowView
- Replace deprecated notify.confirm() with modern $notify() API
- Add structured notification object with group, type, and title properties
- Extract registration prompt response logic into reusable handleRegistrationPromptResponse method
- Update settings method from $updateSettings to $saveSettings for consistency
- Align implementation with ContactsView.vue for better code consistency

This brings the registration prompt notification in ContactQRScanShowView up to parity with the modern implementation used in ContactsView.
2025-09-05 21:39:47 +08:00
Jose Olarte III
f38ec1daff feat: implement seed phrase backup reminder modal
Add comprehensive seed phrase backup reminder system to encourage users
to secure their identity after creating content.

Core Features:
- Modal dialog with "Backup Identifier Seed" and "Remind me Later" options
- 24-hour localStorage cooldown to prevent notification fatigue
- 1-second delay after success messages for better UX flow
- Focuses on claim creation actions, not confirmations

New Files:
- src/utils/seedPhraseReminder.ts: Core utility for reminder logic
- doc/seed-phrase-reminder-implementation.md: Comprehensive documentation

Trigger Points Added:
- Profile saving (AccountViewView)
- Claim creation (ClaimAddRawView, GiftedDialog, GiftedDetailsView)
- Offer creation (OfferDialog)
- QR code view exit (ContactQRScanFullView, ContactQRScanShowView)

Technical Implementation:
- Uses existing notification group modal system from App.vue
- Integrates with PlatformServiceMixin for account settings access
- Graceful error handling with logging fallbacks
- Non-blocking implementation that doesn't affect main functionality
- Modal stays open indefinitely (timeout: -1) until user interaction

User Experience:
- Non-intrusive reminders that respect user preferences
- Clear call-to-action for security-conscious users
- Seamless integration with existing workflows
- Maintains focus on content creation rather than confirmation actions
2025-09-03 19:50:29 +08:00
Jose Olarte III
ec2cab768b feat: Add seed backup tracking with database migration
- Add hasBackedUpSeed boolean flag to Settings interface
- Create database migration 003_add_hasBackedUpSeed_to_settings
- Update SeedBackupView to set flag when user reveals seed phrase
- Modify DataExportSection to conditionally show notification dot
- Implement robust error handling for database operations

The notification dot on the "Backup Identifier Seed" button only
appears while the user hasn't backed up their seed phrase. Once they
visit SeedBackupView and click "Reveal my Seed Phrase", the setting
is persisted and the notification dot disappears.
2025-09-03 15:52:29 +08:00
25 changed files with 636 additions and 170 deletions

View File

@@ -0,0 +1,181 @@
# Seed Phrase Backup Reminder Implementation
## Overview
This implementation adds a modal dialog that reminds users to back up their seed phrase if they haven't done so yet. The reminder appears after specific user actions and includes a 24-hour cooldown to avoid being too intrusive.
## Features
- **Modal Dialog**: Uses the existing notification group modal system from `App.vue`
- **Smart Timing**: Only shows when `hasBackedUpSeed = false`
- **24-Hour Cooldown**: Uses localStorage to prevent showing more than once per day
- **Action-Based Triggers**: Shows after specific user actions
- **User Choice**: "Backup Identifier Seed" or "Remind me Later" options
## Implementation Details
### Core Utility (`src/utils/seedPhraseReminder.ts`)
The main utility provides:
- `shouldShowSeedReminder(hasBackedUpSeed)`: Checks if reminder should be shown
- `markSeedReminderShown()`: Updates localStorage timestamp
- `createSeedReminderNotification()`: Creates the modal configuration
- `showSeedPhraseReminder(hasBackedUpSeed, notifyFunction)`: Main function to show reminder
### Trigger Points
The reminder is shown after these user actions:
**Note**: The reminder is triggered by **claim creation** actions, not claim confirmations. This focuses on when users are actively creating new content rather than just confirming existing claims.
1. **Profile Saving** (`AccountViewView.vue`)
- After clicking "Save Profile" button
- Only when profile save is successful
2. **Claim Creation** (Multiple views)
- `ClaimAddRawView.vue`: After submitting raw claims
- `GiftedDialog.vue`: After creating gifts/claims
- `GiftedDetailsView.vue`: After recording gifts/claims
- `OfferDialog.vue`: After creating offers
3. **QR Code Views Exit**
- `ContactQRScanFullView.vue`: When exiting via back button
- `ContactQRScanShowView.vue`: When exiting via back button
### Modal Configuration
```typescript
{
group: "modal",
type: "confirm",
title: "Backup Your Identifier Seed?",
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
yesText: "Backup Identifier Seed",
noText: "Remind me Later",
onYes: () => navigate to /seed-backup,
onNo: () => mark as shown for 24 hours,
onCancel: () => mark as shown for 24 hours
}
```
**Important**: The modal is configured with `timeout: -1` to ensure it stays open until the user explicitly interacts with one of the buttons. This prevents the dialog from closing automatically.
### Cooldown Mechanism
- **Storage Key**: `seedPhraseReminderLastShown`
- **Cooldown Period**: 24 hours (24 * 60 * 60 * 1000 milliseconds)
- **Implementation**: localStorage with timestamp comparison
- **Fallback**: Shows reminder if timestamp is invalid or missing
## User Experience
### When Reminder Appears
- User has not backed up their seed phrase (`hasBackedUpSeed = false`)
- At least 24 hours have passed since last reminder
- User performs one of the trigger actions
- **1-second delay** after the success message to allow users to see the confirmation
### User Options
1. **"Backup Identifier Seed"**: Navigates to `/seed-backup` page
2. **"Remind me Later"**: Dismisses and won't show again for 24 hours
3. **Cancel/Close**: Same behavior as "Remind me Later"
### Frequency Control
- **First Time**: Always shows if user hasn't backed up
- **Subsequent**: Only shows after 24-hour cooldown
- **Automatic Reset**: When user completes seed backup (`hasBackedUpSeed = true`)
## Technical Implementation
### Error Handling
- Graceful fallback if localStorage operations fail
- Logging of errors for debugging
- Non-blocking implementation (doesn't affect main functionality)
### Integration Points
- **Platform Service**: Uses `$accountSettings()` to check backup status
- **Notification System**: Integrates with existing `$notify` system
- **Router**: Uses `window.location.href` for navigation
### Performance Considerations
- Minimal localStorage operations
- No blocking operations
- Efficient timestamp comparisons
- **Timing Behavior**: 1-second delay before showing reminder to improve user experience flow
## Testing
### Manual Testing Scenarios
1. **First Time User**
- Create new account
- Perform trigger action (save profile, create claim, exit QR view)
- Verify reminder appears
2. **Repeat User (Within 24h)**
- Perform trigger action
- Verify reminder does NOT appear
3. **Repeat User (After 24h)**
- Wait 24+ hours
- Perform trigger action
- Verify reminder appears again
4. **User Who Has Backed Up**
- Complete seed backup
- Perform trigger action
- Verify reminder does NOT appear
5. **QR Code View Exit**
- Navigate to QR code view (full or show)
- Exit via back button
- Verify reminder appears (if conditions are met)
### Browser Testing
- Test localStorage functionality
- Verify timestamp handling
- Check navigation to seed backup page
## Future Enhancements
### Potential Improvements
1. **Customizable Cooldown**: Allow users to set reminder frequency
2. **Progressive Urgency**: Increase reminder frequency over time
3. **Analytics**: Track reminder effectiveness and user response
4. **A/B Testing**: Test different reminder messages and timing
### Configuration Options
- Reminder frequency settings
- Custom reminder messages
- Different trigger conditions
- Integration with other notification systems
## Maintenance
### Monitoring
- Check localStorage usage in browser dev tools
- Monitor user feedback about reminder frequency
- Track navigation success to seed backup page
### Updates
- Modify reminder text in `createSeedReminderNotification()`
- Adjust cooldown period in `REMINDER_COOLDOWN_MS` constant
- Add new trigger points as needed
## Conclusion
This implementation provides a non-intrusive way to remind users about seed phrase backup while respecting their preferences and avoiding notification fatigue. The 24-hour cooldown ensures users aren't overwhelmed while maintaining the importance of the security reminder.
The feature is fully integrated with the existing codebase architecture and follows established patterns for notifications, error handling, and user interaction.

View File

@@ -16,6 +16,12 @@ messages * - Conditional UI based on platform capabilities * * @component *
:to="{ name: 'seed-backup' }"
:class="backupButtonClasses"
>
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
<font-awesome
v-if="!hasBackedUpSeed"
icon="circle"
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
></font-awesome>
Backup Identifier Seed
</router-link>
@@ -98,6 +104,12 @@ export default class DataExportSection extends Vue {
*/
isExporting = false;
/**
* Flag indicating if the user has backed up their seed phrase
* Used to control the visibility of the notification dot
*/
hasBackedUpSeed = false;
/**
* Notification helper for consistent notification patterns
* Created as a getter to ensure $notify is available when called
@@ -129,7 +141,7 @@ export default class DataExportSection extends Vue {
* CSS classes for the backup button (router link)
*/
get backupButtonClasses(): string {
return "block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2";
return "block relative w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2";
}
/**
@@ -218,6 +230,22 @@ export default class DataExportSection extends Vue {
created() {
this.notify = createNotifyHelpers(this.$notify);
this.loadSeedBackupStatus();
}
/**
* Loads the seed backup status from account settings
* Updates the hasBackedUpSeed flag to control notification dot visibility
*/
private async loadSeedBackupStatus(): Promise<void> {
try {
const settings = await this.$accountSettings();
this.hasBackedUpSeed = !!settings.hasBackedUpSeed;
} catch (err: unknown) {
logger.error("Failed to load seed backup status:", err);
// Default to false (show notification dot) if we can't load the setting
this.hasBackedUpSeed = false;
}
}
}
</script>

View File

@@ -82,6 +82,7 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
@@ -411,6 +412,15 @@ export default class GiftedDialog extends Vue {
);
} else {
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
if (this.callbackOnSuccess) {
this.callbackOnSuccess(amount);
}

View File

@@ -74,7 +74,7 @@
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', deepLinkUrl)"
@click="copyTextToClipboard('A link to this page', deepLinkUrl)"
>click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.</a
>
@@ -110,7 +110,7 @@
* @since 2024-12-19
*/
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import * as R from "ramda";
import * as serverUtil from "../libs/endorserServer";
import { Contact } from "../db/tables/contacts";
@@ -197,19 +197,24 @@ export default class HiddenDidDialog extends Vue {
);
}
copyToClipboard(name: string, text: string) {
useClipboard()
.copy(text)
.then(() => {
async copyTextToClipboard(name: string, text: string) {
try {
await copyToClipboard(text);
this.notify.success(
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
TIMEOUTS.SHORT,
);
});
} catch (error) {
this.$logAndConsole(
`Error copying ${name || "content"} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
}
}
onClickShareClaim() {
this.copyToClipboard("A link to this page", this.deepLinkUrl);
this.copyTextToClipboard("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?",

View File

@@ -64,6 +64,7 @@ import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_OFFER_SETTINGS_ERROR,
NOTIFY_OFFER_RECORDING,
@@ -299,6 +300,14 @@ export default class OfferDialog extends Vue {
);
} else {
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {

View File

@@ -124,6 +124,12 @@ const MIGRATIONS = [
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
`,
},
{
name: "003_add_hasBackedUpSeed_to_settings",
sql: `
ALTER TABLE settings ADD COLUMN hasBackedUpSeed BOOLEAN DEFAULT FALSE;
`,
},
];
/**

View File

@@ -29,6 +29,7 @@ export type Settings = {
finishedOnboarding?: boolean; // the user has completed the onboarding process
firstName?: string; // user's full name, may be null if unwanted for a particular account
hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable

View File

@@ -3,7 +3,7 @@
import axios, { AxiosResponse } from "axios";
import { Buffer } from "buffer";
import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
import { Account, AccountEncrypted } from "../db/tables/accounts";
@@ -232,11 +232,19 @@ export const nameForContact = (
);
};
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
export const doCopyTwoSecRedo = async (
text: string,
fn: () => void,
): Promise<void> => {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
// Note: This utility function doesn't have access to notification system
// The calling component should handle error notifications
// Error is silently caught to avoid breaking the 2-second redo pattern
}
};
export interface ConfirmerData {

View File

@@ -0,0 +1,90 @@
import { NotificationIface } from "@/constants/app";
const SEED_REMINDER_KEY = "seedPhraseReminderLastShown";
const REMINDER_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
/**
* Checks if the seed phrase backup reminder should be shown
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
* @returns true if the reminder should be shown, false otherwise
*/
export function shouldShowSeedReminder(hasBackedUpSeed: boolean): boolean {
// Don't show if user has already backed up
if (hasBackedUpSeed) {
return false;
}
// Check localStorage for last shown time
const lastShown = localStorage.getItem(SEED_REMINDER_KEY);
if (!lastShown) {
return true; // First time, show the reminder
}
try {
const lastShownTime = parseInt(lastShown, 10);
const now = Date.now();
const timeSinceLastShown = now - lastShownTime;
// Show if more than 24 hours have passed
return timeSinceLastShown >= REMINDER_COOLDOWN_MS;
} catch (error) {
// If there's an error parsing the timestamp, show the reminder
return true;
}
}
/**
* Marks the seed phrase reminder as shown by updating localStorage
*/
export function markSeedReminderShown(): void {
localStorage.setItem(SEED_REMINDER_KEY, Date.now().toString());
}
/**
* Creates the seed phrase backup reminder notification
* @returns NotificationIface configuration for the reminder modal
*/
export function createSeedReminderNotification(): NotificationIface {
return {
group: "modal",
type: "confirm",
title: "Backup Your Identifier Seed?",
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
yesText: "Backup Identifier Seed",
noText: "Remind me Later",
onYes: async () => {
// Navigate to seed backup page
window.location.href = "/seed-backup";
},
onNo: async () => {
// Mark as shown so it won't appear again for 24 hours
markSeedReminderShown();
},
onCancel: async () => {
// Mark as shown so it won't appear again for 24 hours
markSeedReminderShown();
},
};
}
/**
* Shows the seed phrase backup reminder if conditions are met
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
* @param notifyFunction - Function to show notifications
* @returns true if the reminder was shown, false otherwise
*/
export function showSeedPhraseReminder(
hasBackedUpSeed: boolean,
notifyFunction: (notification: NotificationIface, timeout?: number) => void,
): boolean {
if (shouldShowSeedReminder(hasBackedUpSeed)) {
const notification = createSeedReminderNotification();
// Add 1-second delay before showing the modal to allow success message to be visible
setTimeout(() => {
// Pass -1 as timeout to ensure modal stays open until user interaction
notifyFunction(notification, -1);
}, 1000);
return true;
}
return false;
}

View File

@@ -764,7 +764,7 @@ import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { Capacitor } from "@capacitor/core";
@@ -811,6 +811,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
AccountSettings,
isApiError,
@@ -1083,11 +1084,15 @@ export default class AccountViewView extends Vue {
}
// call fn, copy text to the clipboard, then redo fn after 2 seconds
doCopyTwoSecRedo(text: string, fn: () => void): void {
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
this.notify.error("Failed to copy to clipboard.");
}
}
async toggleShowContactAmounts(): Promise<void> {
@@ -1695,6 +1700,14 @@ export default class AccountViewView extends Vue {
);
if (success) {
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
} else {
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
}

View File

@@ -41,6 +41,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
// Type guard for API responses
function isApiResponse(response: unknown): response is AxiosResponse {
@@ -223,6 +224,14 @@ export default class ClaimAddRawView extends Vue {
);
if (result.success) {
this.notify.success("Claim submitted.", TIMEOUTS.LONG);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
} else {
logger.error("Got error submitting the claim:", result);
this.notify.error(

View File

@@ -58,7 +58,7 @@
title="Copy Printable Certificate Link"
aria-label="Copy printable certificate link"
@click="
copyToClipboard(
copyTextToClipboard(
'A link to the certificate page',
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
)
@@ -72,7 +72,9 @@
<button
title="Copy Link"
aria-label="Copy page link"
@click="copyToClipboard('A link to this page', windowDeepLink)"
@click="
copyTextToClipboard('A link to this page', windowDeepLink)
"
>
<font-awesome icon="link" class="text-slate-500" />
</button>
@@ -399,7 +401,7 @@
contacts can see more details:
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowDeepLink)"
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -422,7 +424,7 @@
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowDeepLink)"
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -532,7 +534,7 @@ import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { GenericVerifiableCredential } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
@@ -1129,16 +1131,21 @@ export default class ClaimView extends Vue {
);
}
copyToClipboard(name: string, text: string) {
useClipboard()
.copy(text)
.then(() => {
async copyTextToClipboard(name: string, text: string) {
try {
await copyToClipboard(text);
this.notify.copied(name || "That");
});
} catch (error) {
this.$logAndConsole(
`Error copying ${name || "content"} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
}
}
onClickShareClaim() {
this.copyToClipboard("A link to this page", this.windowDeepLink);
this.copyTextToClipboard("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?",

View File

@@ -192,7 +192,7 @@
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
<button
@click="
copyToClipboard(
copyTextToClipboard(
'The DID of ' + confirmerId,
confirmerId,
)
@@ -238,7 +238,7 @@
>
<button
@click="
copyToClipboard(
copyTextToClipboard(
'The DID of ' + confsVisibleTo,
confsVisibleTo,
)
@@ -309,7 +309,9 @@
contacts can see more details:
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
@click="
copyTextToClipboard('A link to this page', windowLocation)
"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -332,7 +334,9 @@
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
@click="
copyTextToClipboard('A link to this page', windowLocation)
"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -360,7 +364,7 @@
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<button
@click="
copyToClipboard('The DID of ' + visDid, visDid)
copyTextToClipboard('The DID of ' + visDid, visDid)
"
>
<font-awesome
@@ -433,7 +437,7 @@
import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -779,16 +783,21 @@ export default class ConfirmGiftView extends Vue {
* @param description - Description of copied content
* @param text - Text to copy
*/
copyToClipboard(description: string, text: string): void {
useClipboard()
.copy(text)
.then(() => {
async copyTextToClipboard(description: string, text: string): Promise<void> {
try {
await copyToClipboard(text);
this.notify.toast(
NOTIFY_COPIED_TO_CLIPBOARD.title,
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
TIMEOUTS.SHORT,
);
});
} catch (error) {
this.$logAndConsole(
`Error copying ${description} to clipboard: ${error}`,
true,
);
this.notify.error(`Failed to copy ${description} to clipboard.`);
}
}
/**
@@ -870,7 +879,7 @@ export default class ConfirmGiftView extends Vue {
* Handles share functionality based on platform capabilities
*/
async onClickShareClaim(): Promise<void> {
this.copyToClipboard("A link to this page", this.windowLocation);
this.copyTextToClipboard("A link to this page", this.windowLocation);
window.navigator.share({
title: "Help Connect Me",
text: "I'm trying to find the full details of this claim. Can you help me?",

View File

@@ -104,7 +104,7 @@ import { Buffer } from "buffer/";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { logger } from "../utils/logger";
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
@@ -144,6 +144,7 @@ import {
QR_TIMEOUT_LONG,
} from "@/constants/notifications";
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
interface QRScanResult {
rawValue?: string;
@@ -195,7 +196,7 @@ export default class ContactQRScanFull extends Vue {
$router!: Router;
// Notification helper system
private notify = createNotifyHelpers(this.$notify);
private notify!: ReturnType<typeof createNotifyHelpers>;
isScanning = false;
error: string | null = null;
@@ -263,6 +264,9 @@ export default class ContactQRScanFull extends Vue {
* Loads user settings and generates QR code for contact sharing
*/
async created() {
// Initialize notification helper system
this.notify = createNotifyHelpers(this.$notify);
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
@@ -622,6 +626,15 @@ export default class ContactQRScanFull extends Vue {
*/
async handleBack() {
await this.cleanupScanner();
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
this.$router.back();
}
@@ -636,6 +649,7 @@ export default class ContactQRScanFull extends Vue {
* Copies contact URL to clipboard for sharing
*/
async onCopyUrlToClipboard() {
try {
const account = (await libsUtil.retrieveFullyDecryptedAccount(
this.activeDid,
)) as Account;
@@ -646,26 +660,40 @@ export default class ContactQRScanFull extends Vue {
this.profileImageUrl,
true,
);
useClipboard()
.copy(jwtUrl)
.then(() => {
// Use the platform-specific ClipboardService for reliable iOS support
await copyToClipboard(jwtUrl);
this.notify.toast(
NOTIFY_QR_URL_COPIED.title,
NOTIFY_QR_URL_COPIED.message,
QR_TIMEOUT_MEDIUM,
);
} catch (error) {
logger.error("Error copying URL to clipboard:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error("Failed to copy URL to clipboard.");
}
}
/**
* Copies DID to clipboard for manual sharing
*/
onCopyDidToClipboard() {
useClipboard()
.copy(this.activeDid)
.then(() => {
async onCopyDidToClipboard() {
try {
// Use the platform-specific ClipboardService for reliable iOS support
await copyToClipboard(this.activeDid);
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
} catch (error) {
logger.error("Error copying DID to clipboard:", {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
this.notify.error("Failed to copy DID to clipboard.");
}
}
/**

View File

@@ -140,6 +140,7 @@ import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { copyToClipboard } from "../services/ClipboardService";
import { QrcodeStream } from "vue-qrcode-reader";
@@ -163,6 +164,7 @@ import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
import { CameraState } from "@/services/QRScanner/types";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_QR_INITIALIZATION_ERROR,
NOTIFY_QR_CAMERA_IN_USE,
@@ -319,6 +321,15 @@ export default class ContactQRScanShow extends Vue {
async handleBack(): Promise<void> {
await this.cleanupScanner();
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
this.$router.back();
}
@@ -618,7 +629,6 @@ export default class ContactQRScanShow extends Vue {
);
// Copy the URL to clipboard
const { copyToClipboard } = await import("../services/ClipboardService");
await copyToClipboard(jwtUrl);
this.notify.toast(
NOTIFY_QR_URL_COPIED.title,
@@ -637,7 +647,6 @@ export default class ContactQRScanShow extends Vue {
async onCopyDidToClipboard() {
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
try {
const { copyToClipboard } = await import("../services/ClipboardService");
await copyToClipboard(this.activeDid);
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
} catch (error) {
@@ -738,24 +747,17 @@ export default class ContactQRScanShow extends Vue {
!contact.registered
) {
setTimeout(() => {
this.notify.confirm(
"Do you want to register them?",
this.$notify(
{
group: "modal",
type: "confirm",
title: "Register",
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$updateSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
await this.handleRegistrationPromptResponse(stopAsking);
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
await this.$updateSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
await this.handleRegistrationPromptResponse(stopAsking);
},
onYes: async () => {
await this.register(contact);
@@ -885,6 +887,17 @@ export default class ContactQRScanShow extends Vue {
videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none";
}
}
private async handleRegistrationPromptResponse(
stopAsking?: boolean,
): Promise<void> {
if (stopAsking) {
await this.$saveSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
this.hideRegisterPromptOnNewContact = stopAsking;
}
}
}
</script>

View File

@@ -1003,7 +1003,7 @@
<h2>Exported Data</h2>
<span
class="text-blue-500 cursor-pointer hover:text-blue-700"
@click="copyToClipboard"
@click="copyExportedDataToClipboard"
>
Copy to Clipboard
</span>
@@ -1014,7 +1014,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { Router } from "vue-router";
import {
@@ -1072,8 +1072,6 @@ export default class DatabaseMigration extends Vue {
private exportedData: Record<string, any> | null = null;
private successMessage = "";
useClipboard = useClipboard;
/**
* Computed property to get the display name for a setting
* Handles both live comparison data and exported JSON format
@@ -1133,13 +1131,11 @@ export default class DatabaseMigration extends Vue {
/**
* Copies exported data to clipboard and shows success message
*/
async copyToClipboard(): Promise<void> {
async copyExportedDataToClipboard(): Promise<void> {
if (!this.exportedData) return;
try {
await this.useClipboard().copy(
JSON.stringify(this.exportedData, null, 2),
);
await copyToClipboard(JSON.stringify(this.exportedData, null, 2));
// Use global window object properly
if (typeof window !== "undefined") {
window.alert("Copied to clipboard!");

View File

@@ -280,6 +280,7 @@ import { logger } from "../utils/logger";
import { Contact } from "@/db/tables/contacts";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import {
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
@@ -770,6 +771,15 @@ export default class GiftedDetails extends Vue {
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
TIMEOUTS.SHORT,
);
// Show seed phrase backup reminder if needed
try {
const settings = await this.$accountSettings();
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
} catch (error) {
logger.error("Error checking seed backup status:", error);
}
localStorage.removeItem("imageUrl");
if (this.destinationPathAfter) {
(this.$router as Router).push({ path: this.destinationPathAfter });

View File

@@ -584,15 +584,16 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
// Capacitor import removed - using QRNavigationService instead
import * as Package from "../../package.json";
import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER } from "../constants/app";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { QRNavigationService } from "@/services/QRNavigationService";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
/**
* HelpView.vue - Comprehensive Help System Component
@@ -626,8 +627,10 @@ import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
})
export default class HelpView extends Vue {
$router!: Router;
$notify!: (notification: NotificationIface, timeout?: number) => void;
package = Package;
notify!: ReturnType<typeof createNotifyHelpers>;
commitHash = import.meta.env.VITE_GIT_HASH;
showAlpha = false;
showBasics = false;
@@ -640,6 +643,13 @@ export default class HelpView extends Vue {
APP_SERVER = APP_SERVER;
// Capacitor reference removed - using QRNavigationService instead
/**
* Initialize notification helpers
*/
created() {
this.notify = createNotifyHelpers(this.$notify);
}
/**
* Get the unnamed entity name constant
*/
@@ -660,11 +670,15 @@ export default class HelpView extends Vue {
* @param {string} text - The text to copy to clipboard
* @param {Function} fn - Callback function to execute before and after copying
*/
doCopyTwoSecRedo(text: string, fn: () => void): void {
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
this.notify.error("Failed to copy to clipboard.", TIMEOUTS.SHORT);
}
}
/**

View File

@@ -128,7 +128,7 @@
<script lang="ts">
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { Router } from "vue-router";
import ContactNameDialog from "../components/ContactNameDialog.vue";
@@ -333,17 +333,27 @@ export default class InviteOneView extends Vue {
return `${APP_SERVER}/deep-link/invite-one-accept/${jwt}`;
}
copyInviteAndNotify(inviteId: string, jwt: string) {
useClipboard().copy(this.inviteLink(jwt));
async copyInviteAndNotify(inviteId: string, jwt: string) {
try {
await copyToClipboard(this.inviteLink(jwt));
this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
} catch (error) {
this.$logAndConsole(`Error copying invite link: ${error}`, true);
this.notify.error("Failed to copy invite link.");
}
}
showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
useClipboard().copy(inviteId);
async showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
try {
await copyToClipboard(inviteId);
this.notify.success(
createInviteIdCopyMessage(inviteId, redeemed, expired),
TIMEOUTS.LONG,
);
} catch (error) {
this.$logAndConsole(`Error copying invite ID: ${error}`, true);
this.notify.error("Failed to copy invite ID.");
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -270,7 +270,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
@@ -676,12 +676,17 @@ export default class OnboardMeetingView extends Vue {
this.notify.error(message, TIMEOUTS.LONG);
}
copyMembersLinkToClipboard() {
useClipboard()
.copy(this.onboardMeetingMembersLink())
.then(() => {
async copyMembersLinkToClipboard() {
try {
await copyToClipboard(this.onboardMeetingMembersLink());
this.notify.info(NOTIFY_MEETING_LINK_COPIED.message, TIMEOUTS.LONG);
});
} catch (error) {
this.$logAndConsole(
`Error copying meeting link to clipboard: ${error}`,
true,
);
this.notify.error("Failed to copy meeting link to clipboard.");
}
}
}
</script>

View File

@@ -616,7 +616,7 @@ import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
import { logger } from "../utils/logger";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
@@ -817,7 +817,7 @@ export default class ProjectViewView extends Vue {
});
}
onCopyLinkClick() {
async onCopyLinkClick() {
const shortestProjectId = this.projectId.startsWith(
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
)
@@ -825,11 +825,13 @@ export default class ProjectViewView extends Vue {
: this.projectId;
// Use production URL for sharing to avoid localhost issues in development
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
useClipboard()
.copy(deepLink)
.then(() => {
try {
await copyToClipboard(deepLink);
this.notify.copied("link to this project", TIMEOUTS.SHORT);
});
} catch (error) {
this.$logAndConsole(`Error copying project link: ${error}`, true);
this.notify.error("Failed to copy project link.");
}
}
// Isn't there a better way to make this available to the template?

View File

@@ -127,7 +127,7 @@ import { DateTime } from "luxon";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -305,23 +305,21 @@ export default class QuickActionBvcEndView extends Vue {
(this.$router as Router).push(route);
}
copyContactsLinkToClipboard() {
async copyContactsLinkToClipboard() {
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
useClipboard()
.copy(deepLinkUrl)
.then(() => {
try {
await copyToClipboard(deepLinkUrl);
this.notify.success(
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
TIMEOUTS.SHORT,
);
})
.catch((error) => {
} catch (error) {
logger.error("Failed to copy to clipboard:", error);
this.notify.error(
"Failed to copy link to clipboard. Please try again.",
TIMEOUTS.SHORT,
);
});
}
}
async record() {

View File

@@ -106,7 +106,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -231,9 +231,24 @@ export default class SeedBackupView extends Vue {
/**
* Reveals the seed phrase to the user
* Sets showSeed to true to display the sensitive seed phrase data
* Updates the hasBackedUpSeed setting to true to track that user has backed up
*/
revealSeed(): void {
async revealSeed(): Promise<void> {
this.showSeed = true;
// Update the account setting to track that user has backed up their seed
try {
const settings = await this.$accountSettings();
if (settings.activeDid) {
await this.$saveUserSettings(settings.activeDid, {
hasBackedUpSeed: true,
});
}
} catch (err: unknown) {
logger.error("Failed to update hasBackedUpSeed setting:", err);
// Don't show error to user as this is not critical to the main functionality
// The seed phrase is still revealed, just the tracking won't work
}
}
/**
@@ -264,11 +279,15 @@ export default class SeedBackupView extends Vue {
* @param text - The text to copy to clipboard
* @param fn - Callback function to execute for feedback (called twice - immediately and after 2 seconds)
*/
doCopyTwoSecRedo(text: string, fn: () => void) {
async doCopyTwoSecRedo(text: string, fn: () => void) {
fn();
useClipboard()
.copy(text)
.then(() => setTimeout(fn, 2000));
try {
await copyToClipboard(text);
setTimeout(fn, 2000);
} catch (error) {
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
this.notify.error("Failed to copy to clipboard.");
}
}
}
</script>

View File

@@ -54,6 +54,7 @@ import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { Settings } from "@/db/tables/settings";
import { Account } from "@/db/tables/accounts";
import { copyToClipboard } from "../services/ClipboardService";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
// Constants for magic numbers
@@ -99,7 +100,7 @@ export default class ShareMyContactInfoView extends Vue {
}
const message = await this.generateContactMessage(settings, account);
await this.copyToClipboard(message);
await copyToClipboard(message);
await this.showSuccessNotifications();
this.navigateToContacts();
} catch (error) {
@@ -140,14 +141,6 @@ export default class ShareMyContactInfoView extends Vue {
);
}
/**
* Copy the contact message to clipboard
*/
private async copyToClipboard(message: string): Promise<void> {
const { copyToClipboard } = await import("../services/ClipboardService");
await copyToClipboard(message);
}
/**
* Show success notifications after copying
*/

View File

@@ -108,7 +108,7 @@ import { didInfo, getHeaders } from "../libs/endorserServer";
import { UserProfile } from "../libs/partnerServer";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { useClipboard } from "@vueuse/core";
import { copyToClipboard } from "../services/ClipboardService";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_PROFILE_LOAD_ERROR } from "@/constants/notifications";
@@ -240,14 +240,16 @@ export default class UserProfileView extends Vue {
* Creates a deep link to the profile and copies it to the clipboard
* Shows success notification when completed
*/
onCopyLinkClick() {
async onCopyLinkClick() {
// Use production URL for sharing to avoid localhost issues in development
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
useClipboard()
.copy(deepLink)
.then(() => {
try {
await copyToClipboard(deepLink);
this.notify.copied("profile link", TIMEOUTS.STANDARD);
});
} catch (error) {
this.$logAndConsole(`Error copying profile link: ${error}`, true);
this.notify.error("Failed to copy profile link.");
}
}
/**